, state: &mut H) -> io::Result<()>
+where
+ H: std::hash::Hasher,
+{
+ let file = std::fs::File::open(path)?;
+ let mut file = std::io::BufReader::new(file);
+ let mut buffer: MaybeUninit<[u8; 1024]> = MaybeUninit::uninit();
+ unsafe {
+ let buffer = buffer.as_mut_ptr().as_mut().unwrap_unchecked();
+ loop {
+ let count = file.read(buffer)?;
+ if count == 0 {
+ break;
+ }
+ buffer[..count].hash(state)
+ }
+ }
+ Ok(())
+}
+
+pub fn hash_files(files: impl IntoIterator- ) -> u64
+where
+ P: AsRef,
+{
+ let mut state = xxhash_rust::xxh3::Xxh3::new();
+
+ // We need to discover files separately because listing a directory doesn't
+ // need to return files in the same order every time.
+ let mut discovered = BTreeSet::new();
+ for root in files {
+ let root = root.as_ref();
+ if root.is_file() {
+ discovered.insert(root.to_path_buf());
+ continue;
+ } else {
+ let walk = walkdir::WalkDir::new(root);
+ for item in walk.into_iter().filter_map(Result::ok) {
+ if item.file_type().is_file() {
+ discovered.insert(item.into_path());
+ }
+ }
+ }
+ }
+
+ // Once we have a sorted list of discovered files, we can hash them.
+ for file in discovered {
+ let _ = hash_single_file(file, &mut state);
+ }
+
+ state.finish()
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct FileSize(u64);
+impl FileSize {
+ pub fn of(file: impl AsRef) -> Result {
+ let file = file.as_ref();
+ if !exists(file) {
+ return Err(CommandError::file_not_found("file", file).program("FileSize"));
+ }
+
+ let mut file = match std::fs::File::open(file) {
+ Ok(it) => it,
+ Err(_) => return Ok(FileSize(0)),
+ };
+ Ok(FileSize(
+ file.seek(io::SeekFrom::End(0)).unwrap_or_default(),
+ ))
+ }
+}
+impl From for FileSize {
+ fn from(value: u64) -> Self {
+ FileSize(value)
+ }
+}
+impl From for u64 {
+ fn from(value: FileSize) -> Self {
+ value.0
+ }
+}
+impl From for usize {
+ fn from(value: FileSize) -> Self {
+ value.0 as usize
+ }
+}
+impl Display for FileSize {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ const BYTES_PER_KIB: u64 = 1024;
+ const BYTES_PER_MIB: u64 = BYTES_PER_KIB * 1024;
+ const BYTES_PER_GIB: u64 = BYTES_PER_MIB * 1024;
+ if self.0 / BYTES_PER_GIB >= 1 {
+ write!(f, "{:.3} GiB", self.0 as f32 / BYTES_PER_GIB as f32)
+ } else if self.0 / BYTES_PER_MIB >= 1 {
+ write!(f, "{:.3} MiB", self.0 as f32 / BYTES_PER_MIB as f32)
+ } else if self.0 / BYTES_PER_KIB >= 1 {
+ write!(f, "{:.3} KiB", self.0 as f32 / BYTES_PER_KIB as f32)
+ } else {
+ write!(f, "{} B", self.0)
+ }
+ }
+}
+
+#[allow(dead_code)]
+trait ProgramExt {
+ fn program_status(self, program: impl AsRef) -> Result<(), CommandError>;
+ fn program_output(self, program: impl AsRef) -> proc::Output;
+}
+impl ProgramExt for proc::Command {
+ fn program_status(mut self, program: impl AsRef) -> Result<(), CommandError> {
+ match self.status() {
+ Ok(it) => CommandError::from_exit(it),
+ Err(_) => panic!("unable to run {}", program.as_ref()),
+ }
+ }
+ fn program_output(mut self, program: impl AsRef) -> proc::Output {
+ match self.output() {
+ Ok(it) => it,
+ Err(_) => panic!("unable to run {}", program.as_ref()),
+ }
+ }
+}
+
+pub enum CommandError {
+ MissingTool {
+ program: &'static str,
+ install_from: Option<&'static str>,
+ },
+ ExitError {
+ program: Option<&'static str>,
+ code: std::num::NonZeroI32,
+ },
+ Interrupted {
+ program: Option<&'static str>,
+ interrupt: i32,
+ },
+ BadArgument {
+ program: Option<&'static str>,
+ argument: &'static str,
+ expect_found: Option<(&'static str, Box)>,
+ reason: Option>,
+ },
+}
+
+impl CommandError {
+ pub fn new(code: i32) -> Self {
+ assert!(code != 0, "exit code 0 doesn't indicate an error");
+ unsafe {
+ Self::ExitError {
+ program: None,
+ code: std::num::NonZeroI32::new_unchecked(code),
+ }
+ }
+ }
+ pub fn interrupt(interrupt: i32) -> Self {
+ Self::Interrupted {
+ program: None,
+ interrupt,
+ }
+ }
+ pub fn missing_tool(name: &'static str, source: Option<&'static str>) -> Self {
+ Self::MissingTool {
+ program: name,
+ install_from: source,
+ }
+ }
+ pub fn file_not_found(argument: &'static str, file: impl AsRef) -> Self {
+ Self::BadArgument {
+ program: None,
+ argument,
+ expect_found: None,
+ reason: Some(Box::new(io::Error::new(
+ io::ErrorKind::NotFound,
+ format!(
+ "file not found or inaccessible (path: '{}')",
+ file.as_ref().display()
+ ),
+ ))),
+ }
+ }
+ pub fn inaccessible(argument: &'static str, source: io::Error) -> Self {
+ Self::BadArgument {
+ program: None,
+ argument,
+ expect_found: None,
+ reason: Some(Box::new(source)),
+ }
+ }
+
+ pub fn program(mut self, name: &'static str) -> Self {
+ match &mut self {
+ CommandError::MissingTool { program, .. } => *program = name,
+ CommandError::ExitError { program, .. } => *program = Some(name),
+ CommandError::Interrupted { program, .. } => *program = Some(name),
+ CommandError::BadArgument { program, .. } => *program = Some(name),
+ }
+ self
+ }
+
+ pub fn from_exit(exit: proc::ExitStatus) -> Result<(), Self> {
+ #[allow(unreachable_patterns)]
+ match exit.code() {
+ Some(0) => Ok(()),
+ Some(code) => Err(Self::new(code)),
+ #[cfg(unix)]
+ None => Err(Self::interrupt(
+ std::os::unix::prelude::ExitStatusExt::signal(&exit)
+ .expect("program terminated with no exit code, nor interrupt signal"),
+ )),
+ _ => unreachable!("program terminated with no exit code"),
+ }
+ }
+}
+
+impl From for CommandError {
+ fn from(exit: proc::ExitStatus) -> Self {
+ Self::from_exit(exit).expect_err("not an error")
+ }
+}
+
+impl Debug for CommandError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let mut result = f.debug_struct(match self {
+ CommandError::MissingTool { .. } => "CommandError::MissingTool",
+ CommandError::ExitError { .. } => "CommandError::ExitError",
+ CommandError::Interrupted { .. } => "CommandError::Interrupted",
+ CommandError::BadArgument { .. } => "CommandError::BadArgument",
+ });
+ match self {
+ CommandError::MissingTool { program, .. } => {
+ result.field("program", program);
+ }
+ CommandError::ExitError { program, code } => {
+ result.field("program", program);
+ result.field("code", code);
+ }
+ CommandError::Interrupted { program, interrupt } => {
+ result.field("program", program);
+ result.field("interrupt", interrupt);
+ }
+ CommandError::BadArgument {
+ program,
+ argument,
+ expect_found,
+ reason,
+ } => {
+ result.field("program", program);
+ result.field("argument", argument);
+ if let Some((expected, found)) = expect_found {
+ result.field("expected", expected);
+ result.field("found", &found.as_ref().to_string());
+ } else if let Some(reason) = reason {
+ result.field("reason", reason);
+ }
+ }
+ }
+ result.finish()
+ }
+}
+
+impl Display for CommandError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ CommandError::MissingTool {
+ program,
+ install_from,
+ } => {
+ write!(f, "{program} is not in PATH, and it's required for running requested tasks. Install it using {} or from",
+ if cfg!(target_os = "macos") {
+ "brew"
+ } else if cfg!(target_os = "windows") {
+ "win-get"
+ } else {
+ "a package manager"
+ }
+ )?;
+ if let Some(from) = install_from {
+ write!(f, ": '{from}'")
+ } else {
+ write!(f, " a credible source.")
+ }
+ }
+ CommandError::ExitError { program, code } => {
+ write!(
+ f,
+ "{} exited with code #{}",
+ program.unwrap_or("process"),
+ code
+ )
+ }
+ CommandError::Interrupted { program, interrupt } => {
+ write!(
+ f,
+ "{} interrupted (signal: {})",
+ program.unwrap_or("process"),
+ interrupt
+ )
+ }
+ CommandError::BadArgument {
+ program,
+ argument,
+ expect_found,
+ reason,
+ } => {
+ let detail = if let Some((expected, found)) = expect_found {
+ format!(" {expected} expected, but found {found}")
+ } else if let Some(why) = reason {
+ format!(": {why}")
+ } else {
+ "".to_string()
+ };
+ write!(
+ f,
+ "{}bad '{argument}' argument{detail}",
+ program
+ .map(|p| format!("{p} executed with "))
+ .unwrap_or_default()
+ )
+ }
+ }
+ }
+}
+
+impl std::error::Error for CommandError {
+ fn cause(&self) -> Option<&dyn std::error::Error> {
+ match self {
+ CommandError::BadArgument {
+ reason: Some(reason),
+ ..
+ } => Some(reason.as_ref()),
+ _ => None,
+ }
+ }
+}
diff --git a/xtask/src/util.rs b/xtask/src/util.rs
new file mode 100644
index 0000000..653e47d
--- /dev/null
+++ b/xtask/src/util.rs
@@ -0,0 +1,77 @@
+use std::ffi::{OsStr, OsString};
+
+/// Does the same as [str::replace], only for `byte`s intead of `char`s.
+pub trait SliceReplace::Owned> {
+ fn replace_slices(&self, from: &Borrowed, to: &Borrowed) -> Owned;
+}
+impl SliceReplace for [u8] {
+ fn replace_slices(&self, from: &[u8], to: &[u8]) -> Vec {
+ if self.len() < from.len() {
+ return self.to_vec();
+ }
+ let mut result = Vec::with_capacity(if from.len() <= to.len() {
+ self.len()
+ } else {
+ (self.len() / from.len() * to.len()) // assume we'll replace everything
+ .min(self.len() * 2) // but don't over-allocate
+ });
+
+ let mut pos = 0;
+ while pos < self.len() {
+ if pos + from.len() > self.len() {
+ result.extend_from_slice(&self[pos..]);
+ break;
+ }
+
+ if &self[pos..pos + from.len()] == from {
+ result.extend_from_slice(to);
+ pos += from.len();
+ } else {
+ result.push(self[pos]);
+ pos += 1;
+ }
+ }
+ result
+ }
+}
+impl SliceReplace<[u8]> for Vec {
+ #[inline(always)]
+ fn replace_slices(&self, from: &[u8], to: &[u8]) -> Vec {
+ self.as_slice().replace_slices(from, to)
+ }
+}
+impl SliceReplace for OsStr {
+ fn replace_slices(&self, from: &OsStr, to: &OsStr) -> OsString {
+ unsafe {
+ // SAFETY: All inputs were encoded
+ OsString::from_encoded_bytes_unchecked(
+ self.as_encoded_bytes()
+ .replace_slices(from.as_encoded_bytes(), to.as_encoded_bytes()),
+ )
+ }
+ }
+}
+impl SliceReplace for &OsStr {
+ #[inline(always)]
+ fn replace_slices(&self, from: &OsStr, to: &OsStr) -> OsString {
+ (*self).replace_slices(from, to)
+ }
+}
+impl SliceReplace for OsString {
+ #[inline(always)]
+ fn replace_slices(&self, from: &OsStr, to: &OsStr) -> OsString {
+ self.as_os_str().replace_slices(from, to)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn slice_replace_works() {
+ let input = vec![1, 2, 3, 4, 5];
+ let output = input.replace_slices(&[2, 3], &[6, 7, 8]);
+ assert_eq!(output, &[1, 6, 7, 8, 4, 5])
+ }
+}
diff --git a/xtask/state b/xtask/state
new file mode 100644
index 0000000..63881d3
--- /dev/null
+++ b/xtask/state
@@ -0,0 +1,10 @@
+MANUAL_HASH=6768760725972220257
+PLUGIN_STUB_OPT_WASM=zint_typst_plugin_stub_opt.wasm
+PLUGIN_STUB_WASM=zint_typst_plugin_stub.wasm
+PLUGIN_WASM=zint_typst_plugin.wasm
+PLUGIN_WASM_HASH=13965701503739485256
+PLUGIN_WASM_OUT=zint_typst_plugin.wasm
+PLUGIN_WASM_STUB_HASH=13965701503739485256
+TARGET=wasm32-wasip1
+TYPST_PKG=$/typst-package
+WORK_DIR=$/target
diff --git a/zint-typst-plugin/.cargo/config.toml b/zint-typst-plugin/.cargo/config.toml
new file mode 100644
index 0000000..6b509f5
--- /dev/null
+++ b/zint-typst-plugin/.cargo/config.toml
@@ -0,0 +1,2 @@
+[build]
+target = "wasm32-wasip1"
diff --git a/zint-typst-plugin/Cargo.toml b/zint-typst-plugin/Cargo.toml
index 8baa64e..0aff6d2 100644
--- a/zint-typst-plugin/Cargo.toml
+++ b/zint-typst-plugin/Cargo.toml
@@ -17,4 +17,4 @@ ciborium = "0.2.1"
thiserror = "1.0"
-wasm-minimal-protocol = { git = "https://github.com/astrale-sharp/wasm-minimal-protocol" }
+wasm-minimal-protocol = { path = "./vendor/wasm-minimal-protocol/crates/macro" }
diff --git a/zint-typst-plugin/vendor/wasm-minimal-protocol b/zint-typst-plugin/vendor/wasm-minimal-protocol
new file mode 160000
index 0000000..787cc5b
--- /dev/null
+++ b/zint-typst-plugin/vendor/wasm-minimal-protocol
@@ -0,0 +1 @@
+Subproject commit 787cc5ba3cf85908b03b40568213902b8e7f474d
diff --git a/zint-wasm-sys/Cargo.toml b/zint-wasm-sys/Cargo.toml
index c979707..1049859 100644
--- a/zint-wasm-sys/Cargo.toml
+++ b/zint-wasm-sys/Cargo.toml
@@ -6,8 +6,11 @@ description = "Rust bindings for Wasm build of zint"
license = "MIT"
categories = ["external-ffi-bindings"]
+[target.'cfg(not(target_family = "wasm"))'.dependencies]
+libc = "0.2"
+
[build-dependencies]
cc = "1.0"
bindgen = "0.70.1"
-walkdir = "2"
+walkdir = { workspace = true }
anyhow = "1"
diff --git a/zint-wasm-sys/build.rs b/zint-wasm-sys/build.rs
index ff9cb99..2e3a8f4 100644
--- a/zint-wasm-sys/build.rs
+++ b/zint-wasm-sys/build.rs
@@ -1,11 +1,58 @@
use anyhow::Result;
-use std::{env, path::PathBuf};
+use std::{
+ env,
+ path::{Path, PathBuf},
+};
use walkdir::WalkDir;
+fn watch_files(path: impl AsRef) {
+ for entry in WalkDir::new(path).into_iter().filter_map(Result::ok) {
+ if entry.file_type().is_file() {
+ println!("cargo:rerun-if-changed={}", entry.path().display());
+ }
+ }
+}
+
fn main() -> Result<()> {
- env::set_var("CC", "/opt/wasi-sdk/bin/clang");
- env::set_var("AR", "/opt/wasi-sdk/bin/ar");
+ #[allow(non_snake_case)]
+ let WASM = env::var("CARGO_CFG_TARGET_FAMILY")
+ .map(|it| it == "wasm")
+ .unwrap_or_default();
+ #[allow(non_snake_case)]
+ let WASM32_WASIP1 = WASM
+ && env::var("TARGET")
+ .map(|it| it == "wasm32-wasip1")
+ .unwrap_or_default();
+
+ if WASM32_WASIP1 {
+ let sdk_path = match env::var("WASI_SDK_PATH") {
+ Ok(it) => PathBuf::from(it),
+ Err(_) => PathBuf::from("/opt/wasi-sdk"),
+ };
+ // report these errors early with clear error messages
+ match std::fs::exists(&sdk_path) {
+ Ok(true) => {}
+ Ok(false) => panic!(
+ "WASI SDK not installed, misconfigured: {}",
+ sdk_path.display()
+ ),
+ Err(_) => panic!(
+ "missing permissions to access WASI SDK: {}",
+ sdk_path.display()
+ ),
+ }
+
+ let sdk_bin = sdk_path.join("bin");
+ let sdk_sysroot = sdk_path.join("share/wasi-sysroot");
+
+ unsafe {
+ env::set_var("CC", sdk_bin.join("clang"));
+ env::set_var("AR", sdk_bin.join("ar"));
+ env::set_var("CFLAGS", format!("--sysroot={}", sdk_sysroot.display()));
+ }
+ }
+
let files = [
"zint/backend/2of5.c",
"zint/backend/auspost.c",
@@ -56,11 +103,13 @@ fn main() -> Result<()> {
"zint/backend/vector.c",
"patch/patch.c",
];
- // Build quickjs as a static library.
- cc::Build::new()
- .files(files.iter())
+
+ // Build zint as a static library.
+ let mut build = cc::Build::new();
+
+ build
+ .files(files)
.define("_GNU_SOURCE", None)
- .cargo_metadata(true)
// The below flags are used by the official Makefile.
.flag_if_supported("-Wchar-subscripts")
.flag_if_supported("-Wno-array-bounds")
@@ -77,27 +126,31 @@ fn main() -> Result<()> {
.flag_if_supported("-Wno-enum-conversion")
.flag_if_supported("-Wno-implicit-function-declaration")
.flag_if_supported("-Wno-implicit-const-int-float-conversion")
- .target("wasm32-wasip1")
- .opt_level(2)
- .compile("zint");
+ .flag_if_supported("-Wno-shift-op-parentheses")
+ .opt_level(2);
- // Generate bindings for quickjs
+ if WASM32_WASIP1 {
+ build.target("wasm32-wasip1");
+ }
+ build.compile("zint");
+
+ // Generate bindings for zint
let bindings = bindgen::Builder::default()
.header("zint/backend/zint.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
- .clang_args(&["-fvisibility=hidden", "--target=wasm32-wasip1"])
- .size_t_is_usize(false)
- .generate()?;
+ .clang_arg("-fvisibility=hidden")
+ .size_t_is_usize(false);
- println!("cargo:rerun-if-changed=wrapper.h");
+ let bindings = if WASM32_WASIP1 {
+ bindings.clang_arg("--target=wasm32-wasip1")
+ } else {
+ bindings
+ };
- for entry in WalkDir::new("zint") {
- println!("cargo:rerun-if-changed={}", entry?.path().display());
- }
+ let bindings = bindings.generate()?;
- for entry in WalkDir::new("patch") {
- println!("cargo:rerun-if-changed={}", entry?.path().display());
- }
+ watch_files("zint");
+ watch_files("patch");
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
bindings.write_to_file(out_dir.join("bindings.rs"))?;
diff --git a/zint-wasm-sys/vendored/libclang_rt.builtins-wasm32-wasi-20.0.tar.gz b/zint-wasm-sys/vendored/libclang_rt.builtins-wasm32-wasi-20.0.tar.gz
deleted file mode 100644
index 1dd0be3..0000000
Binary files a/zint-wasm-sys/vendored/libclang_rt.builtins-wasm32-wasi-20.0.tar.gz and /dev/null differ
diff --git a/zint-wasm-sys/wrapper.h b/zint-wasm-sys/wrapper.h
deleted file mode 100644
index e69de29..0000000