Move asset hashing into the CLI (#3988)
* Fix deduping assets * hash assets in the linker * fix extracting assets * Fix the hashed asset name * deserialize the asset back from the link section * prevent the read from being inlined * remove some extra logs * Fix liveview routing * Only look for direct dependencies on dioxus features * fix wasm split test * On wasm, create a new object file with the asset information * Fix multiple assets * add a regression test for 3467 * fix image link * fix linker out file * fix linux linker args * add windows linker command * fix linux link args * fix windows target triple * Expose an option to build assets in the linker intercept * Fix the standalone optimizer and document it in the dx help command * include the cli opt version in the hash * fix clippy * fix ssg port * add a safety note to the read_volatile call * move found assets log * fix some merge conflicts * fix build * make most env var for the linker optional * guess the target triple when dx acts as the linker intercept * automatically find the linker for normal builds * use the right linker in wasm mode * fix mac linker * look inside archive files (rlibs) * remove out.txt * fix clippy * fix typo * fix clippy * fix build * fix dx link name in docs * ignore libcompiler_builtins warnings * fix playwright * pass typocheck * fix printing sysroot * Modify final binary * fix wasm assets * remove old link section logic * fix tests * forward manganis export args * fail to evaluate relocatable address * retain all exports in the same pattern as other platforms * Fix adding assets during a wasm hot patch * less nesting and remove logs * fix clippy * log pdb files * read from pdb * print data section contents * print with object * normal windows builds working * fix hot patch windows * clean up logs * fix clippy * clean up assets * use the most recent pdb * fix doc test * fix typo * tiny nits * make bundled a reference instead of a pointer * add helpful comment to impl * match on file format * Fix asset hashing comment --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
f865e3f559
commit
58669f892d
45 files changed
+1174
-762
No files matched your search
@@ -1,23 +1,13 @@
|
||||
use crate::AssetOptions;
|
||||
use const_serialize::{ConstStr, SerializeConst};
|
||||
use std::path::PathBuf;
|
||||
use const_serialize::{deserialize_const, ConstStr, ConstVec, SerializeConst};
|
||||
use std::{fmt::Debug, hash::Hash, path::PathBuf};
|
||||
|
||||
/// An asset that should be copied by the bundler with some options. This type will be
|
||||
/// serialized into the binary and added to the link section [`LinkSection::CURRENT`](crate::linker::LinkSection::CURRENT).
|
||||
/// serialized into the binary.
|
||||
/// CLIs that support manganis, should pull out the assets from the link section, optimize,
|
||||
/// and write them to the filesystem at [`BundledAsset::bundled_path`] for the application
|
||||
/// to use.
|
||||
#[derive(
|
||||
Debug,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Clone,
|
||||
Copy,
|
||||
Hash,
|
||||
SerializeConst,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
#[derive(Debug, Eq, Clone, Copy, SerializeConst, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BundledAsset {
|
||||
/// The absolute path of the asset
|
||||
absolute_source_path: ConstStr,
|
||||
@@ -27,13 +17,46 @@ pub struct BundledAsset {
|
||||
options: AssetOptions,
|
||||
}
|
||||
|
||||
impl PartialEq for BundledAsset {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.absolute_source_path == other.absolute_source_path
|
||||
&& self.bundled_path == other.bundled_path
|
||||
&& self.options == other.options
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for BundledAsset {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
match self
|
||||
.absolute_source_path
|
||||
.partial_cmp(&other.absolute_source_path)
|
||||
{
|
||||
Some(core::cmp::Ordering::Equal) => {}
|
||||
ord => return ord,
|
||||
}
|
||||
match self.bundled_path.partial_cmp(&other.bundled_path) {
|
||||
Some(core::cmp::Ordering::Equal) => {}
|
||||
ord => return ord,
|
||||
}
|
||||
self.options.partial_cmp(&other.options)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for BundledAsset {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.absolute_source_path.hash(state);
|
||||
self.bundled_path.hash(state);
|
||||
self.options.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl BundledAsset {
|
||||
#[doc(hidden)]
|
||||
/// This should only be called from the macro
|
||||
/// Create a new asset
|
||||
pub const fn new(
|
||||
absolute_source_path: &'static str,
|
||||
bundled_path: &'static str,
|
||||
absolute_source_path: &str,
|
||||
bundled_path: &str,
|
||||
options: AssetOptions,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -83,6 +106,7 @@ impl BundledAsset {
|
||||
pub fn absolute_source_path(&self) -> &str {
|
||||
self.absolute_source_path.as_str()
|
||||
}
|
||||
|
||||
/// Get the options for the asset
|
||||
pub const fn options(&self) -> &AssetOptions {
|
||||
&self.options
|
||||
@@ -101,28 +125,49 @@ impl BundledAsset {
|
||||
/// img { src: ASSET }
|
||||
/// };
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
pub struct Asset {
|
||||
/// The bundled asset
|
||||
bundled: BundledAsset,
|
||||
/// The link section for the asset
|
||||
keep_link_section: fn() -> u8,
|
||||
/// A pointer to the bundled asset. This will be resolved after the linker has run and
|
||||
/// put into the lazy asset
|
||||
///
|
||||
/// WARNING: Don't read this directly. Reads can get optimized away at compile time before
|
||||
/// the data for this is filled in by the CLI after the binary is built. Instead, use
|
||||
/// [`std::ptr::read_volatile`] to read the data.
|
||||
bundled: &'static [u8],
|
||||
}
|
||||
|
||||
impl Debug for Asset {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.resolve().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for Asset {}
|
||||
unsafe impl Sync for Asset {}
|
||||
|
||||
impl Asset {
|
||||
#[doc(hidden)]
|
||||
/// This should only be called from the macro
|
||||
/// Create a new asset from the bundled form of the asset and the link section
|
||||
pub const fn new(bundled: BundledAsset, keep_link_section: fn() -> u8) -> Self {
|
||||
Self {
|
||||
bundled,
|
||||
keep_link_section,
|
||||
}
|
||||
pub const fn new(bundled: &'static [u8]) -> Self {
|
||||
Self { bundled }
|
||||
}
|
||||
|
||||
/// Get the bundled asset
|
||||
pub const fn bundled(&self) -> &BundledAsset {
|
||||
&self.bundled
|
||||
pub fn bundled(&self) -> BundledAsset {
|
||||
let len = self.bundled.len();
|
||||
let ptr = self.bundled as *const [u8] as *const u8;
|
||||
if ptr.is_null() {
|
||||
panic!("Tried to use an asset that was not bundled. Make sure you are compiling dx as the linker");
|
||||
}
|
||||
let mut bytes = ConstVec::new();
|
||||
for byte in 0..len {
|
||||
// SAFETY: We checked that the pointer was not null above. The pointer is valid for reads and
|
||||
// since we are reading a u8 there are no alignment requirements
|
||||
bytes = bytes.push(unsafe { std::ptr::read_volatile(ptr.add(byte)) });
|
||||
}
|
||||
let read = bytes.read();
|
||||
deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1
|
||||
}
|
||||
|
||||
/// Return a canonicalized path to the asset
|
||||
@@ -130,13 +175,10 @@ impl Asset {
|
||||
/// Attempts to resolve it against an `assets` folder in the current directory.
|
||||
/// If that doesn't exist, it will resolve against the cargo manifest dir
|
||||
pub fn resolve(&self) -> PathBuf {
|
||||
// Force a volatile read of the asset link section to ensure the symbol makes it into the binary
|
||||
(self.keep_link_section)();
|
||||
|
||||
#[cfg(feature = "dioxus")]
|
||||
// If the asset is relative, we resolve the asset at the current directory
|
||||
if !dioxus_core_types::is_bundled_app() {
|
||||
return PathBuf::from(self.bundled.absolute_source_path.as_str());
|
||||
return PathBuf::from(self.bundled().absolute_source_path.as_str());
|
||||
}
|
||||
|
||||
#[cfg(feature = "dioxus")]
|
||||
@@ -156,7 +198,7 @@ impl Asset {
|
||||
|
||||
// Otherwise presumably we're bundled and we can use the bundled path
|
||||
bundle_root.join(PathBuf::from(
|
||||
self.bundled.bundled_path.as_str().trim_start_matches('/'),
|
||||
self.bundled().bundled_path.as_str().trim_start_matches('/'),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::collections::HashSet;
|
||||
/// Options for a css asset
|
||||
#[derive(
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Clone,
|
||||
@@ -79,6 +80,7 @@ impl CssAssetOptions {
|
||||
/// Options for a css module asset
|
||||
#[derive(
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Clone,
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::AssetOptions;
|
||||
/// The builder for a folder asset.
|
||||
#[derive(
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Clone,
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
//! Utilities for creating hashed paths to assets in Manganis. This module defines [`AssetHash`] which is used to create a hashed path to an asset in both the CLI and the macro.
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
hash::{Hash, Hasher},
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// An error that can occur when hashing an asset
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum AssetHashError {
|
||||
/// An io error occurred
|
||||
IoError { err: std::io::Error, path: PathBuf },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AssetHashError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AssetHashError::IoError { path, err } => {
|
||||
write!(f, "Failed to read file: {}; {}", path.display(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AssetHashError {}
|
||||
|
||||
/// The opaque hash type manganis uses to identify assets. Each time an asset or asset options change, this hash will
|
||||
/// change. This hash is included in the URL of the bundled asset for cache busting.
|
||||
pub struct AssetHash {
|
||||
/// We use a wrapper type here to hide the exact size of the hash so we can switch to a sha hash in a minor version bump
|
||||
hash: [u8; 8],
|
||||
}
|
||||
|
||||
impl AssetHash {
|
||||
/// Create a new asset hash
|
||||
const fn new(hash: u64) -> Self {
|
||||
Self {
|
||||
hash: hash.to_le_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the hash bytes
|
||||
pub const fn bytes(&self) -> &[u8] {
|
||||
&self.hash
|
||||
}
|
||||
|
||||
/// Create a new asset hash for a file. The input file to this function should be fully resolved
|
||||
pub fn hash_file_contents(file_path: &Path) -> Result<AssetHash, AssetHashError> {
|
||||
// Create a hasher
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
|
||||
// If this is a folder, hash the folder contents
|
||||
if file_path.is_dir() {
|
||||
let files = std::fs::read_dir(file_path).map_err(|err| AssetHashError::IoError {
|
||||
err,
|
||||
path: file_path.to_path_buf(),
|
||||
})?;
|
||||
for file in files.flatten() {
|
||||
let path = file.path();
|
||||
Self::hash_file_contents(&path)?.bytes().hash(&mut hash);
|
||||
}
|
||||
let hash = hash.finish();
|
||||
return Ok(AssetHash::new(hash));
|
||||
}
|
||||
|
||||
// Otherwise, open the file to get its contents
|
||||
let mut file = std::fs::File::open(file_path).map_err(|err| AssetHashError::IoError {
|
||||
err,
|
||||
path: file_path.to_path_buf(),
|
||||
})?;
|
||||
|
||||
// We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
|
||||
// The hash includes the file contents, the options, and the version of manganis. From the macro, we just
|
||||
// know the file contents, so we only include that hash
|
||||
let mut buffer = [0; 8192];
|
||||
loop {
|
||||
let read = file
|
||||
.read(&mut buffer)
|
||||
.map_err(|err| AssetHashError::IoError {
|
||||
err,
|
||||
path: file_path.to_path_buf(),
|
||||
})?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
hash.write(&buffer[..read]);
|
||||
}
|
||||
|
||||
Ok(AssetHash::new(hash.finish()))
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use crate::AssetOptions;
|
||||
/// The type of an image. You can read more about the tradeoffs between image formats [here](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types)
|
||||
#[derive(
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Clone,
|
||||
@@ -31,6 +32,7 @@ pub enum ImageFormat {
|
||||
/// The size of an image asset
|
||||
#[derive(
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Clone,
|
||||
@@ -56,6 +58,7 @@ pub enum ImageSize {
|
||||
/// Options for an image asset
|
||||
#[derive(
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Clone,
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::AssetOptions;
|
||||
/// Options for a javascript asset
|
||||
#[derive(
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Clone,
|
||||
|
||||
@@ -15,7 +15,3 @@ pub use js::*;
|
||||
|
||||
mod asset;
|
||||
pub use asset::*;
|
||||
|
||||
pub mod linker;
|
||||
|
||||
pub mod hash;
|
||||
@@ -1,71 +0,0 @@
|
||||
//! Utilities for working with Manganis assets in the linker. This module defines [`LinkSection`] which has information about what section manganis assets are stored in on each platform.
|
||||
|
||||
/// Information about the manganis link section for a given platform
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LinkSection {
|
||||
/// The link section we pass to the static
|
||||
pub link_section: &'static str,
|
||||
/// The name of the section we find in the binary
|
||||
pub name: &'static str,
|
||||
}
|
||||
|
||||
impl LinkSection {
|
||||
/// The list of link sections for all supported platforms
|
||||
pub const ALL: &'static [&'static LinkSection] =
|
||||
&[Self::WASM, Self::MACOS, Self::WINDOWS, Self::ILLUMOS];
|
||||
|
||||
/// Returns the link section used in linux, android, fuchsia, psp, freebsd, and wasm32
|
||||
pub const WASM: &'static LinkSection = &LinkSection {
|
||||
link_section: "manganis",
|
||||
name: "manganis",
|
||||
};
|
||||
|
||||
/// Returns the link section used in macOS, iOS, tvOS
|
||||
pub const MACOS: &'static LinkSection = &LinkSection {
|
||||
link_section: "__DATA,manganis,regular,no_dead_strip",
|
||||
name: "manganis",
|
||||
};
|
||||
|
||||
/// Returns the link section used in windows
|
||||
pub const WINDOWS: &'static LinkSection = &LinkSection {
|
||||
link_section: "mg",
|
||||
name: "mg",
|
||||
};
|
||||
|
||||
/// Returns the link section used in illumos
|
||||
pub const ILLUMOS: &'static LinkSection = &LinkSection {
|
||||
link_section: "set_manganis",
|
||||
name: "set_manganis",
|
||||
};
|
||||
|
||||
/// The link section used on the current platform
|
||||
pub const CURRENT: &'static LinkSection = {
|
||||
#[cfg(any(
|
||||
target_os = "none",
|
||||
target_os = "linux",
|
||||
target_os = "android",
|
||||
target_os = "fuchsia",
|
||||
target_os = "psp",
|
||||
target_os = "freebsd",
|
||||
target_arch = "wasm32"
|
||||
))]
|
||||
{
|
||||
Self::WASM
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))]
|
||||
{
|
||||
Self::MACOS
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Self::WINDOWS
|
||||
}
|
||||
|
||||
#[cfg(target_os = "illumos")]
|
||||
{
|
||||
Self::ILLUMOS
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
/// Settings for a generic asset
|
||||
#[derive(
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Clone,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::{resolve_path, AssetParseError};
|
||||
use macro_string::MacroString;
|
||||
use manganis_core::hash::AssetHash;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
path::PathBuf,
|
||||
};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
spanned::Spanned as _,
|
||||
@@ -72,24 +74,19 @@ impl ToTokens for AssetParser {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let asset_str = asset.to_string_lossy();
|
||||
let mut asset_str = proc_macro2::Literal::string(&asset_str);
|
||||
let asset_string = asset.to_string_lossy();
|
||||
let mut asset_str = proc_macro2::Literal::string(&asset_string);
|
||||
asset_str.set_span(self.path_expr.span());
|
||||
|
||||
let hash = match AssetHash::hash_file_contents(asset) {
|
||||
Ok(hash) => hash,
|
||||
Err(err) => {
|
||||
let err = err.to_string();
|
||||
tokens.append_all(quote! { compile_error!(#err) });
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let hash = hash.bytes();
|
||||
let mut hash = DefaultHasher::new();
|
||||
format!("{:?}", self.options.span()).hash(&mut hash);
|
||||
format!("{:?}", self.options.to_string()).hash(&mut hash);
|
||||
asset_string.hash(&mut hash);
|
||||
let asset_hash = format!("{:016x}", hash.finish());
|
||||
|
||||
// Generate the link section for the asset
|
||||
// The link section includes the source path and the output path of the asset
|
||||
let link_section = crate::generate_link_section(quote!(__ASSET));
|
||||
let link_section = crate::generate_link_section(quote!(__ASSET), &asset_hash);
|
||||
|
||||
// generate the asset::new method to deprecate the `./assets/blah.css` syntax
|
||||
let constructor = if asset.is_relative() {
|
||||
@@ -106,21 +103,25 @@ impl ToTokens for AssetParser {
|
||||
|
||||
tokens.extend(quote! {
|
||||
{
|
||||
// We keep a hash of the contents of the asset for cache busting
|
||||
const __ASSET_HASH: &[u8] = &[#(#hash),*];
|
||||
// The source is used by the CLI to copy the asset
|
||||
const __ASSET_SOURCE_PATH: &'static str = #asset_str;
|
||||
// The options give the CLI info about how to process the asset
|
||||
// Note: into_asset_options is not a trait, so we cannot accept the options directly
|
||||
// in the constructor. Stable rust doesn't have support for constant functions in traits
|
||||
const __ASSET_OPTIONS: manganis::AssetOptions = #options.into_asset_options();
|
||||
// The input token hash is used to uniquely identify the link section for this asset
|
||||
const __ASSET_HASH: &'static str = #asset_hash;
|
||||
// Create the asset that the crate will use. This is used both in the return value and
|
||||
// added to the linker for the bundler to copy later
|
||||
const __ASSET: manganis::BundledAsset = manganis::macro_helpers::#constructor(__ASSET_SOURCE_PATH, __ASSET_HASH, __ASSET_OPTIONS);
|
||||
const __ASSET: manganis::BundledAsset = manganis::macro_helpers::#constructor(__ASSET_SOURCE_PATH, __ASSET_OPTIONS);
|
||||
|
||||
#link_section
|
||||
|
||||
manganis::Asset::new(__ASSET, __keep_link_section)
|
||||
static __REFERENCE_TO_LINK_SECTION: &'static [u8] = &__LINK_SECTION;
|
||||
|
||||
manganis::Asset::new(
|
||||
__REFERENCE_TO_LINK_SECTION
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,12 +6,9 @@ use quote::ToTokens;
|
||||
/// We force rust to store a serialized representation of the asset description
|
||||
/// inside a particular region of the binary, with the label "manganis".
|
||||
/// After linking, the "manganis" sections of the different object files will be merged.
|
||||
pub fn generate_link_section(asset: impl ToTokens) -> TokenStream2 {
|
||||
pub fn generate_link_section(asset: impl ToTokens, asset_hash: &str) -> TokenStream2 {
|
||||
let position = proc_macro2::Span::call_site();
|
||||
let section_name = syn::LitStr::new(
|
||||
manganis_core::linker::LinkSection::CURRENT.link_section,
|
||||
position,
|
||||
);
|
||||
let export_name = syn::LitStr::new(&format!("__MANGANIS__{}", asset_hash), position);
|
||||
|
||||
quote::quote! {
|
||||
// First serialize the asset into a constant sized buffer
|
||||
@@ -22,12 +19,7 @@ pub fn generate_link_section(asset: impl ToTokens) -> TokenStream2 {
|
||||
const __LEN: usize = __BYTES.len();
|
||||
|
||||
// Now that we have the size of the asset, copy the bytes into a static array
|
||||
#[link_section = #section_name]
|
||||
#[used]
|
||||
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
|
||||
|
||||
fn __keep_link_section() -> u8 {
|
||||
unsafe { std::ptr::read_volatile(__LINK_SECTION.as_ptr()) }
|
||||
}
|
||||
#[unsafe(export_name = #export_name)]
|
||||
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
use const_serialize::{serialize_const, ConstVec, SerializeConst};
|
||||
|
||||
// From rustchash - https://github.com/rust-lang/rustc-hash/blob/6745258da00b7251bed4a8461871522d0231a9c7/src/lib.rs#L98
|
||||
const K: u64 = 0xf1357aea2e62a9c5;
|
||||
|
||||
pub(crate) struct ConstHasher {
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
impl ConstHasher {
|
||||
pub const fn new() -> Self {
|
||||
Self { hash: 0 }
|
||||
}
|
||||
|
||||
pub const fn finish(&self) -> u64 {
|
||||
self.hash
|
||||
}
|
||||
|
||||
pub const fn write(mut self, bytes: &[u8]) -> Self {
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
self = self.write_byte(bytes[i]);
|
||||
i += 1;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn write_byte(mut self, byte: u8) -> Self {
|
||||
self.hash = self.hash.wrapping_add(byte as u64).wrapping_mul(K);
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn hash_by_bytes<T: SerializeConst>(self, item: &T) -> Self {
|
||||
let mut bytes = ConstVec::new();
|
||||
bytes = serialize_const(item, bytes);
|
||||
let bytes = bytes.as_ref();
|
||||
self.write(bytes)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod hash;
|
||||
#[doc(hidden)]
|
||||
pub mod macro_helpers;
|
||||
pub use manganis_macro::{asset, css_module};
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
pub use const_serialize;
|
||||
use const_serialize::{serialize_const, ConstStr, ConstVec};
|
||||
use const_serialize::{serialize_const, ConstStr, ConstVec, SerializeConst};
|
||||
use manganis_core::{AssetOptions, BundledAsset};
|
||||
|
||||
use crate::hash::ConstHasher;
|
||||
const PLACEHOLDER_HASH: ConstStr =
|
||||
ConstStr::new("this is a placeholder path which will be replaced by the linker");
|
||||
|
||||
/// Create a bundled asset from the input path, the content hash, and the asset options
|
||||
pub const fn create_bundled_asset(
|
||||
input_path: &str,
|
||||
content_hash: &[u8],
|
||||
asset_config: AssetOptions,
|
||||
) -> BundledAsset {
|
||||
let hashed_path = generate_unique_path_with_byte_hash(input_path, content_hash, &asset_config);
|
||||
BundledAsset::new_from_const(ConstStr::new(input_path), hashed_path, asset_config)
|
||||
pub const fn create_bundled_asset(input_path: &str, asset_config: AssetOptions) -> BundledAsset {
|
||||
BundledAsset::new_from_const(ConstStr::new(input_path), PLACEHOLDER_HASH, asset_config)
|
||||
}
|
||||
|
||||
/// Create a bundled asset from the input path, the content hash, and the asset options with a relative asset deprecation warning
|
||||
@@ -22,204 +18,29 @@ pub const fn create_bundled_asset(
|
||||
)]
|
||||
pub const fn create_bundled_asset_relative(
|
||||
input_path: &str,
|
||||
content_hash: &[u8],
|
||||
asset_config: AssetOptions,
|
||||
) -> BundledAsset {
|
||||
create_bundled_asset(input_path, content_hash, asset_config)
|
||||
}
|
||||
|
||||
/// Format the input path with a hash to create an unique output path for the macro in the form `{input_path}-{hash}.{extension}`
|
||||
pub const fn generate_unique_path(
|
||||
input_path: &str,
|
||||
content_hash: u64,
|
||||
asset_config: &AssetOptions,
|
||||
) -> ConstStr {
|
||||
let byte_hash = content_hash.to_le_bytes();
|
||||
generate_unique_path_with_byte_hash(input_path, &byte_hash, asset_config)
|
||||
}
|
||||
|
||||
/// Format the input path with a hash to create an unique output path for the macro in the form `{input_path}-{hash}.{extension}`
|
||||
const fn generate_unique_path_with_byte_hash(
|
||||
input_path: &str,
|
||||
content_hash: &[u8],
|
||||
asset_config: &AssetOptions,
|
||||
) -> ConstStr {
|
||||
// Format the unique path with the format `{input_path}-{hash}.{extension}`
|
||||
// Start with the input path
|
||||
let mut input_path = ConstStr::new(input_path);
|
||||
// Then strip the prefix from the input path. The path comes from the build platform, but
|
||||
// in wasm, we don't know what the path separator is from the build platform. We need to
|
||||
// split by both unix and windows paths and take the smallest one
|
||||
let mut extension = None;
|
||||
match (input_path.rsplit_once('/'), input_path.rsplit_once('\\')) {
|
||||
(Some((_, unix_new_input_path)), Some((_, windows_new_input_path))) => {
|
||||
input_path = if unix_new_input_path.len() < windows_new_input_path.len() {
|
||||
unix_new_input_path
|
||||
} else {
|
||||
windows_new_input_path
|
||||
};
|
||||
}
|
||||
(Some((_, unix_new_input_path)), _) => {
|
||||
input_path = unix_new_input_path;
|
||||
}
|
||||
(_, Some((_, windows_new_input_path))) => {
|
||||
input_path = windows_new_input_path;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some((new_input_path, new_extension)) = input_path.rsplit_once('.') {
|
||||
extension = Some(new_extension);
|
||||
input_path = new_input_path;
|
||||
}
|
||||
// Then add a dash
|
||||
let mut macro_output_path = input_path.push_str("-");
|
||||
|
||||
// Hash the contents along with the asset config to create a unique hash for the asset
|
||||
// When this hash changes, the client needs to re-fetch the asset
|
||||
let mut hasher = ConstHasher::new();
|
||||
hasher = hasher.write(content_hash);
|
||||
hasher = hasher.hash_by_bytes(asset_config);
|
||||
let hash = hasher.finish();
|
||||
|
||||
// Then add the hash in hex form
|
||||
let hash_bytes = hash.to_le_bytes();
|
||||
let mut i = 0;
|
||||
while i < hash_bytes.len() {
|
||||
let byte = hash_bytes[i];
|
||||
let first = byte >> 4;
|
||||
let second = byte & 0x0f;
|
||||
const fn byte_to_char(byte: u8) -> char {
|
||||
match char::from_digit(byte as u32, 16) {
|
||||
Some(c) => c,
|
||||
None => panic!("byte must be a valid digit"),
|
||||
}
|
||||
}
|
||||
macro_output_path = macro_output_path.push(byte_to_char(first));
|
||||
macro_output_path = macro_output_path.push(byte_to_char(second));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Finally add the extension
|
||||
match asset_config.extension() {
|
||||
Some(extension) => {
|
||||
macro_output_path = macro_output_path.push('.');
|
||||
macro_output_path = macro_output_path.push_str(extension)
|
||||
}
|
||||
None => {
|
||||
if let Some(extension) = extension {
|
||||
macro_output_path = macro_output_path.push('.');
|
||||
|
||||
let ext_bytes = extension.as_str().as_bytes();
|
||||
|
||||
// Rewrite scss as css
|
||||
if bytes_equal(ext_bytes, b"scss") || bytes_equal(ext_bytes, b"sass") {
|
||||
macro_output_path = macro_output_path.push_str("css")
|
||||
} else {
|
||||
macro_output_path = macro_output_path.push_str(extension.as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_output_path
|
||||
}
|
||||
|
||||
/// Construct the hash used by manganis and cli-opt to uniquely identify a asset based on its contents
|
||||
pub const fn hash_asset(asset_config: &AssetOptions, content_hash: u64) -> ConstStr {
|
||||
let mut string = ConstStr::new("");
|
||||
|
||||
// Hash the contents along with the asset config to create a unique hash for the asset
|
||||
// When this hash changes, the client needs to re-fetch the asset
|
||||
let mut hasher = ConstHasher::new();
|
||||
hasher = hasher.write(&content_hash.to_le_bytes());
|
||||
hasher = hasher.hash_by_bytes(asset_config);
|
||||
let hash = hasher.finish();
|
||||
|
||||
// Then add the hash in hex form
|
||||
let hash_bytes = hash.to_le_bytes();
|
||||
let mut i = 0;
|
||||
while i < hash_bytes.len() {
|
||||
let byte = hash_bytes[i];
|
||||
let first = byte >> 4;
|
||||
let second = byte & 0x0f;
|
||||
const fn byte_to_char(byte: u8) -> char {
|
||||
match char::from_digit(byte as u32, 16) {
|
||||
Some(c) => c,
|
||||
None => panic!("byte must be a valid digit"),
|
||||
}
|
||||
}
|
||||
string = string.push(byte_to_char(first));
|
||||
string = string.push(byte_to_char(second));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
string
|
||||
}
|
||||
|
||||
const fn bytes_equal(left: &[u8], right: &[u8]) -> bool {
|
||||
if left.len() != right.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
while i < left.len() {
|
||||
if left[i] != right[i] {
|
||||
return false;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unique_path() {
|
||||
use manganis_core::{ImageAssetOptions, ImageFormat};
|
||||
use std::path::PathBuf;
|
||||
let mut input_path = PathBuf::from("some");
|
||||
input_path.push("prefix");
|
||||
input_path.push("test.png");
|
||||
let content_hash = 123456789;
|
||||
let asset_config = AssetOptions::Image(ImageAssetOptions::new().with_format(ImageFormat::Avif));
|
||||
let output_path =
|
||||
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
|
||||
assert_eq!(output_path.as_str(), "test-603a88fe296462a3.avif");
|
||||
|
||||
// Changing the path without changing the contents shouldn't change the hash
|
||||
let mut input_path = PathBuf::from("some");
|
||||
input_path.push("prefix");
|
||||
input_path.push("prefix");
|
||||
input_path.push("test.png");
|
||||
let content_hash = 123456789;
|
||||
let asset_config = AssetOptions::Image(ImageAssetOptions::new().with_format(ImageFormat::Avif));
|
||||
let output_path =
|
||||
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
|
||||
assert_eq!(output_path.as_str(), "test-603a88fe296462a3.avif");
|
||||
|
||||
let mut input_path = PathBuf::from("test");
|
||||
input_path.push("ing");
|
||||
input_path.push("test");
|
||||
let content_hash = 123456789;
|
||||
let asset_config = AssetOptions::Unknown;
|
||||
let output_path =
|
||||
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
|
||||
assert_eq!(output_path.as_str(), "test-8d6e32dc0b45f853");
|
||||
|
||||
// Just changing the content hash should change the total hash
|
||||
let mut input_path = PathBuf::from("test");
|
||||
input_path.push("ing");
|
||||
input_path.push("test");
|
||||
let content_hash = 123456780;
|
||||
let asset_config = AssetOptions::Unknown;
|
||||
let output_path =
|
||||
generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
|
||||
assert_eq!(output_path.as_str(), "test-40783366737abc4d");
|
||||
create_bundled_asset(input_path, asset_config)
|
||||
}
|
||||
|
||||
/// Serialize an asset to a const buffer
|
||||
pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8> {
|
||||
let write = ConstVec::new();
|
||||
serialize_const(asset, write)
|
||||
let data = ConstVec::new();
|
||||
let mut data = serialize_const(asset, data);
|
||||
// Reserve the maximum size of the asset
|
||||
while data.len() < BundledAsset::MEMORY_LAYOUT.size() {
|
||||
data = data.push(0);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
/// Deserialize a const buffer into a BundledAsset
|
||||
pub const fn deserialize_asset(bytes: &[u8]) -> BundledAsset {
|
||||
let bytes = ConstVec::new().extend(bytes);
|
||||
match const_serialize::deserialize_const!(BundledAsset, bytes.read()) {
|
||||
Some((_, asset)) => asset,
|
||||
None => panic!("Failed to deserialize asset. This may be caused by a mismatch between your dioxus and dioxus-cli versions"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a slice into a constant sized buffer at compile time
|
||||
|
||||
Reference in new issue
Block a user