- inline mac, windows, linux bundling in the cli - remove tauri bundle from our dep tree - trim down the rpm crate for fewer dependencies in tree - migrate from blacksmith to warpbuild - move off swc and instead use esbuild via tool - use dev drives in windows CI - use fork of nix action for faster caching - test bundling in CI - fix asset handling in linux - add default APPDATA location for webview2 installs for dioxus apps on windows
185 lines
6.6 KiB
Rust
185 lines
6.6 KiB
Rust
//! 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::{
|
|
hash::{Hash, Hasher},
|
|
io::Read,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use crate::opt::{
|
|
css::hash_scss,
|
|
file::{resolve_asset_options, ResolvedAssetType},
|
|
js::hash_js,
|
|
};
|
|
use manganis::{AssetOptions, BundledAsset};
|
|
|
|
/// 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.
|
|
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(
|
|
options: &AssetOptions,
|
|
file_path: impl AsRef<Path>,
|
|
) -> anyhow::Result<AssetHash> {
|
|
hash_file(options, file_path.as_ref())
|
|
}
|
|
}
|
|
|
|
/// Process a specific file asset with the given options reading from the source and writing to the output path
|
|
fn hash_file(options: &AssetOptions, source: &Path) -> anyhow::Result<AssetHash> {
|
|
// Create a hasher
|
|
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
|
options.hash(&mut hash);
|
|
|
|
// Hash the version of the CLI
|
|
hash.write(crate::dx_build_info::PKG_VERSION.as_bytes());
|
|
hash_file_with_options(options, source, &mut hash, false)?;
|
|
|
|
let hash = hash.finish();
|
|
Ok(AssetHash::new(hash))
|
|
}
|
|
|
|
/// Process a specific file asset with additional options
|
|
pub(crate) fn hash_file_with_options(
|
|
options: &AssetOptions,
|
|
source: &Path,
|
|
hasher: &mut impl Hasher,
|
|
in_folder: bool,
|
|
) -> anyhow::Result<()> {
|
|
let resolved_options = resolve_asset_options(source, options.variant());
|
|
|
|
match &resolved_options {
|
|
// Scss and JS can import files during the bundling process. We need to hash
|
|
// both the files themselves and any imports they have
|
|
ResolvedAssetType::Scss(options) => {
|
|
hash_scss(options, source, hasher)?;
|
|
}
|
|
|
|
ResolvedAssetType::Js(options) => {
|
|
hash_js(options, source, hasher, !in_folder)?;
|
|
}
|
|
|
|
// Otherwise, we can just hash the file contents
|
|
ResolvedAssetType::CssModule(_)
|
|
| ResolvedAssetType::Css(_)
|
|
| ResolvedAssetType::Image(_)
|
|
| ResolvedAssetType::Json
|
|
| ResolvedAssetType::File => {
|
|
hash_file_contents(source, hasher)?;
|
|
}
|
|
|
|
// Or the folder contents recursively
|
|
ResolvedAssetType::Folder(_) => {
|
|
for file in std::fs::read_dir(source)?.flatten() {
|
|
let path = file.path();
|
|
hash_file_with_options(
|
|
// We can't reuse the options here since they contain the source variant which no
|
|
// longer applies to the nested files
|
|
//
|
|
// We don't hash nested files either since we assume the parent here is already being hashed
|
|
// (or being opted out of hashing)
|
|
&AssetOptions::builder()
|
|
.with_hash_suffix(false)
|
|
.into_asset_options(),
|
|
&path,
|
|
hasher,
|
|
true,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn hash_file_contents(source: &Path, hasher: &mut impl Hasher) -> anyhow::Result<()> {
|
|
// Otherwise, open the file to get its contents
|
|
let mut file = std::fs::File::open(source)?;
|
|
|
|
// 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)?;
|
|
if read == 0 {
|
|
break;
|
|
}
|
|
hasher.write(&buffer[..read]);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Add a hash to the asset, or log an error if it fails
|
|
pub(crate) fn add_hash_to_asset(asset: &mut BundledAsset) {
|
|
let source = asset.absolute_source_path();
|
|
match AssetHash::hash_file_contents(asset.options(), source) {
|
|
Ok(hash) => {
|
|
let options = *asset.options();
|
|
|
|
// Set the bundled path to the source path with the hash appended before the extension
|
|
let source_path = PathBuf::from(source);
|
|
let Some(file_name) = source_path.file_name() else {
|
|
tracing::error!("Failed to get file name from path: {source}");
|
|
return;
|
|
};
|
|
|
|
// The output extension path is the extension set by the options
|
|
// or the extension of the source file if we don't recognize the file
|
|
let mut ext = asset.options().extension().map(Into::into).or_else(|| {
|
|
source_path
|
|
.extension()
|
|
.map(|ext| ext.to_string_lossy().to_string())
|
|
});
|
|
|
|
// Rewrite scss as css
|
|
if let Some("scss" | "sass") = ext.as_deref() {
|
|
ext = Some("css".to_string());
|
|
}
|
|
|
|
let hash = hash.bytes();
|
|
let hash = hash
|
|
.iter()
|
|
.map(|byte| format!("{byte:x}"))
|
|
.collect::<String>();
|
|
let file_stem = source_path.file_stem().unwrap_or(file_name);
|
|
let mut bundled_path = if asset.options().hash_suffix() {
|
|
PathBuf::from(format!("{}-dxh{hash}", file_stem.to_string_lossy()))
|
|
} else {
|
|
PathBuf::from(file_stem)
|
|
};
|
|
|
|
if let Some(ext) = ext {
|
|
// Push the extension to the bundled path. There may be multiple extensions (e.g. .js.map)
|
|
// with one left after the file_stem is extracted above so we need to push the extension
|
|
// instead of setting it
|
|
bundled_path.as_mut_os_string().push(format!(".{ext}"));
|
|
}
|
|
|
|
let bundled_path = bundled_path.to_string_lossy().to_string();
|
|
|
|
*asset = BundledAsset::new(source, &bundled_path, options);
|
|
}
|
|
Err(err) => {
|
|
tracing::error!("Failed to hash asset {source}: {err}");
|
|
}
|
|
}
|
|
}
|