From 956103e8646498c3af3087ab7818979d9f66d0cb Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Tue, 17 Jun 2025 14:41:20 +0100 Subject: [PATCH 1/6] wip python wheels --- Cargo.lock | 150 ++++++++++++++++++++++++++++++++- diff3proof/Cargo.toml | 2 + diff3proof/pyproject.toml | 13 +++ diffenator3-cli/Cargo.toml | 2 + diffenator3-cli/pyproject.toml | 15 ++++ 5 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 diff3proof/pyproject.toml create mode 100644 diffenator3-cli/pyproject.toml diff --git a/Cargo.lock b/Cargo.lock index 194e5b0..9c01359 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,7 +319,7 @@ version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -465,6 +465,7 @@ dependencies = [ "env_logger", "fancy-regex", "google-fonts-languages", + "pyo3", "serde_json", "tera", ] @@ -479,6 +480,7 @@ dependencies = [ "env_logger", "indexmap 1.9.3", "itertools 0.13.0", + "pyo3", "rayon", "serde", "serde_json", @@ -775,6 +777,12 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -903,6 +911,12 @@ dependencies = [ "web-time", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1024,6 +1038,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -1036,6 +1060,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.8.5" @@ -1094,6 +1127,29 @@ dependencies = [ "ttf-parser 0.25.1", ] +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "parse-zoneinfo" version = "0.3.1" @@ -1274,7 +1330,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck", + "heck 0.5.0", "itertools 0.14.0", "log", "multimap", @@ -1403,6 +1459,69 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc55d7dec32ecaf61e0bd90b3d2392d721a28b95cfd23c3e176eccefbeab2f2" +[[package]] +name = "pyo3" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + [[package]] name = "qoi" version = "0.4.1" @@ -1488,6 +1607,15 @@ dependencies = [ "serde", ] +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "regex" version = "1.11.1" @@ -1592,6 +1720,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.219" @@ -1708,6 +1842,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.19.1" @@ -1940,6 +2080,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446c96c6dd42604779487f0a981060717156648c1706aa1f464677f03c6cc059" +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/diff3proof/Cargo.toml b/diff3proof/Cargo.toml index d990c4d..0d791ac 100644 --- a/diff3proof/Cargo.toml +++ b/diff3proof/Cargo.toml @@ -11,3 +11,5 @@ clap = { version = "4.5.9", features = ["derive"] } env_logger = "0.11" serde_json = { workspace = true } fancy-regex = "0.13" + +pyo3 = { version = "0.21", features = ["extension-module"] } diff --git a/diff3proof/pyproject.toml b/diff3proof/pyproject.toml new file mode 100644 index 0000000..437fb5f --- /dev/null +++ b/diff3proof/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[tool.maturin] +# maturin needs a manifest with a [package], can't just use the workspace's Cargo.toml +manifest-path = "Cargo.toml" +# treat the project as a Rust binary application: https://www.maturin.rs/bindings#bin +bindings = "bin" + + +[project] +name = "diff3proof" \ No newline at end of file diff --git a/diffenator3-cli/Cargo.toml b/diffenator3-cli/Cargo.toml index 9ffccf3..c35ae5e 100644 --- a/diffenator3-cli/Cargo.toml +++ b/diffenator3-cli/Cargo.toml @@ -16,3 +16,5 @@ colored = "2.1.0" clap = { version = "4.5.9", features = ["derive"] } itertools = "0.13.0" env_logger = "0.11" + +pyo3 = { version = "0.21", features = ["extension-module"] } diff --git a/diffenator3-cli/pyproject.toml b/diffenator3-cli/pyproject.toml new file mode 100644 index 0000000..bb00258 --- /dev/null +++ b/diffenator3-cli/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[tool.maturin] +# maturin needs a manifest with a [package], can't just use the workspace's Cargo.toml +manifest-path = "Cargo.toml" +# treat the project as a Rust binary application: https://www.maturin.rs/bindings#bin +bindings = "bin" + +[project] +name = "diffenator3_cli" +version = "0.1.0" +description = "Python bindings for diffenator3 CLI" +requires-python = ">=3.7" \ No newline at end of file From 7d92c903628c0f626a13a67b8fad4e09310eaffa Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Tue, 17 Jun 2025 15:06:19 +0100 Subject: [PATCH 2/6] tidy --- diff3proof/pyproject.toml | 1 - diffenator3-cli/pyproject.toml | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/diff3proof/pyproject.toml b/diff3proof/pyproject.toml index 437fb5f..6134f8c 100644 --- a/diff3proof/pyproject.toml +++ b/diff3proof/pyproject.toml @@ -8,6 +8,5 @@ manifest-path = "Cargo.toml" # treat the project as a Rust binary application: https://www.maturin.rs/bindings#bin bindings = "bin" - [project] name = "diff3proof" \ No newline at end of file diff --git a/diffenator3-cli/pyproject.toml b/diffenator3-cli/pyproject.toml index bb00258..519120c 100644 --- a/diffenator3-cli/pyproject.toml +++ b/diffenator3-cli/pyproject.toml @@ -9,7 +9,4 @@ manifest-path = "Cargo.toml" bindings = "bin" [project] -name = "diffenator3_cli" -version = "0.1.0" -description = "Python bindings for diffenator3 CLI" -requires-python = ">=3.7" \ No newline at end of file +name = "diffenator3" \ No newline at end of file From 904171934e4c4336c18ff382872e285bde6c8b21 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Wed, 18 Jun 2025 10:07:17 +0100 Subject: [PATCH 3/6] drop py03 --- Cargo.lock | 150 +------------------------------------ diff3proof/Cargo.toml | 2 - diffenator3-cli/Cargo.toml | 2 - 3 files changed, 2 insertions(+), 152 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c01359..194e5b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,7 +319,7 @@ version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn", @@ -465,7 +465,6 @@ dependencies = [ "env_logger", "fancy-regex", "google-fonts-languages", - "pyo3", "serde_json", "tera", ] @@ -480,7 +479,6 @@ dependencies = [ "env_logger", "indexmap 1.9.3", "itertools 0.13.0", - "pyo3", "rayon", "serde", "serde_json", @@ -777,12 +775,6 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -911,12 +903,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1038,16 +1024,6 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.27" @@ -1060,15 +1036,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "miniz_oxide" version = "0.8.5" @@ -1127,29 +1094,6 @@ dependencies = [ "ttf-parser 0.25.1", ] -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - [[package]] name = "parse-zoneinfo" version = "0.3.1" @@ -1330,7 +1274,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck 0.5.0", + "heck", "itertools 0.14.0", "log", "multimap", @@ -1459,69 +1403,6 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc55d7dec32ecaf61e0bd90b3d2392d721a28b95cfd23c3e176eccefbeab2f2" -[[package]] -name = "pyo3" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" -dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset", - "parking_lot", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn", -] - [[package]] name = "qoi" version = "0.4.1" @@ -1607,15 +1488,6 @@ dependencies = [ "serde", ] -[[package]] -name = "redox_syscall" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" -dependencies = [ - "bitflags 2.9.0", -] - [[package]] name = "regex" version = "1.11.1" @@ -1720,12 +1592,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "serde" version = "1.0.219" @@ -1842,12 +1708,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" version = "3.19.1" @@ -2080,12 +1940,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446c96c6dd42604779487f0a981060717156648c1706aa1f464677f03c6cc059" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "utf8parse" version = "0.2.2" diff --git a/diff3proof/Cargo.toml b/diff3proof/Cargo.toml index 0d791ac..d990c4d 100644 --- a/diff3proof/Cargo.toml +++ b/diff3proof/Cargo.toml @@ -11,5 +11,3 @@ clap = { version = "4.5.9", features = ["derive"] } env_logger = "0.11" serde_json = { workspace = true } fancy-regex = "0.13" - -pyo3 = { version = "0.21", features = ["extension-module"] } diff --git a/diffenator3-cli/Cargo.toml b/diffenator3-cli/Cargo.toml index c35ae5e..9ffccf3 100644 --- a/diffenator3-cli/Cargo.toml +++ b/diffenator3-cli/Cargo.toml @@ -16,5 +16,3 @@ colored = "2.1.0" clap = { version = "4.5.9", features = ["derive"] } itertools = "0.13.0" env_logger = "0.11" - -pyo3 = { version = "0.21", features = ["extension-module"] } From 7ef23ec099591d9a8efe142409f3e23f959d91b3 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Wed, 18 Jun 2025 10:51:33 +0100 Subject: [PATCH 4/6] Cosimo feedback --- diff3proof/pyproject.toml | 2 -- diffenator3-cli/pyproject.toml | 2 -- 2 files changed, 4 deletions(-) diff --git a/diff3proof/pyproject.toml b/diff3proof/pyproject.toml index 6134f8c..9301ebd 100644 --- a/diff3proof/pyproject.toml +++ b/diff3proof/pyproject.toml @@ -3,8 +3,6 @@ requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [tool.maturin] -# maturin needs a manifest with a [package], can't just use the workspace's Cargo.toml -manifest-path = "Cargo.toml" # treat the project as a Rust binary application: https://www.maturin.rs/bindings#bin bindings = "bin" diff --git a/diffenator3-cli/pyproject.toml b/diffenator3-cli/pyproject.toml index 519120c..0cad7ba 100644 --- a/diffenator3-cli/pyproject.toml +++ b/diffenator3-cli/pyproject.toml @@ -3,8 +3,6 @@ requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [tool.maturin] -# maturin needs a manifest with a [package], can't just use the workspace's Cargo.toml -manifest-path = "Cargo.toml" # treat the project as a Rust binary application: https://www.maturin.rs/bindings#bin bindings = "bin" From 2a2c5534b1f184aa5c89c0d11f4950aabf1aba1e Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Fri, 23 Jan 2026 15:14:55 +0000 Subject: [PATCH 5/6] add pypi release --- .github/workflows/release.yml | 237 ++++++++++++++++++++++++++ Cargo.lock | 3 + diff3proof/pyproject.toml | 10 -- diffenator3-cli/Cargo.toml | 17 ++ diffenator3-cli/pyproject.toml | 10 -- diffenator3-cli/src/bin/diff3proof.rs | 213 +++++++++++++++++++++++ pyproject.toml | 20 +++ 7 files changed, 490 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/release.yml delete mode 100644 diff3proof/pyproject.toml delete mode 100644 diffenator3-cli/pyproject.toml create mode 100644 diffenator3-cli/src/bin/diff3proof.rs create mode 100644 pyproject.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..609e4c4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,237 @@ +name: Release + +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build ${{ matrix.platform.target }} + strategy: + fail-fast: false + matrix: + platform: + - target: x86_64-apple-darwin + os: macos-latest + python-architecture: x64 + archive: diffenator3-x86_64-apple-darwin.tar.gz + - target: x86_64-pc-windows-msvc + os: windows-latest + python-architecture: x64 + archive: diffenator3-x86_64-pc-windows-msvc.zip + - target: i686-pc-windows-msvc + os: windows-latest + python-architecture: x86 + archive: diffenator3-i686-pc-windows-msvc.zip + runs-on: ${{ matrix.platform.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + architecture: ${{ matrix.platform.python-architecture }} + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform.target }} + + - name: Install protoc for google-fonts-languages + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + # Install gnu-tar because BSD tar is buggy + # https://github.com/actions/cache/issues/403 + - name: Install GNU tar (macOS) + if: matrix.platform.os == 'macos-latest' + run: | + brew install gnu-tar + echo "/usr/local/opt/gnu-tar/libexec/gnubin" >> $GITHUB_PATH + + - name: Build wheel (macOS universal2) and sdist + if: matrix.platform.target == 'x86_64-apple-darwin' + uses: PyO3/maturin-action@v1 + with: + target: universal2-apple-darwin + args: --release -o dist --sdist + + - name: Build wheel (without sdist) + if: matrix.platform.target != 'x86_64-apple-darwin' + uses: PyO3/maturin-action@v1 + with: + target: ${{matrix.platform.target}} + args: --release -o dist + + - name: Install wheel + shell: bash + run: | + pip install --no-index --find-links dist/ --force-reinstall diffenator3 + which diffenator3 + diffenator3 --version + + - name: Check sdist metadata + if: matrix.platform.target == 'x86_64-apple-darwin' + run: pipx run twine check dist/*.tar.gz + + - name: Archive binary + if: matrix.platform.os != 'windows-latest' + run: | + cd target/${{ matrix.platform.target }}/release + tar czvf ../../../${{ matrix.platform.archive }} diffenator3 diff3proof + cd - + + - name: Archive binary (windows) + if: matrix.platform.os == 'windows-latest' + run: | + cd target/${{ matrix.platform.target }}/release + 7z a ../../../${{ matrix.platform.archive }} diffenator3.exe diff3proof.exe + cd - + + - name: Archive binary (macOS aarch64) + if: matrix.platform.os == 'macos-latest' + run: | + cd target/aarch64-apple-darwin/release + tar czvf ../../../diffenator3-aarch64-apple-darwin.tar.gz diffenator3 diff3proof + cd - + + - name: Upload wheel artifacts + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.platform.target }} + path: dist + + - name: Upload binary artifacts + uses: actions/upload-artifact@v4 + with: + name: binaries-${{ matrix.platform.target }} + path: | + *.tar.gz + *.zip + + build_linux: + name: Build ${{ matrix.platform.target }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + - target: "x86_64-unknown-linux-musl" + image_tag: "x86_64-musl" + compatibility: "manylinux2010 musllinux_1_1" + - target: "aarch64-unknown-linux-musl" + image_tag: "aarch64-musl" + compatibility: "manylinux2014 musllinux_1_1" + container: + image: docker://ghcr.io/rust-cross/rust-musl-cross:${{ matrix.platform.image_tag }} + steps: + - uses: actions/checkout@v3 + + - name: Build Wheels + uses: PyO3/maturin-action@main + with: + target: ${{ matrix.platform.target }} + manylinux: ${{ matrix.platform.compatibility }} + container: off + args: --release -o dist + + - name: Install x86_64 wheel + if: startsWith(matrix.platform.target, 'x86_64') + run: | + /usr/bin/python3 -m pip install --no-index --find-links dist/ --force-reinstall diffenator3 + which diffenator3 + diffenator3 --version + + - name: Archive binary + run: tar czvf target/release/diffenator3-${{ matrix.platform.target }}.tar.gz -C target/${{ matrix.platform.target }}/release diffenator3 diff3proof + + - name: Upload wheel artifacts + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.platform.target }} + path: dist + + - name: Upload binary artifacts + uses: actions/upload-artifact@v4 + with: + name: binaries-${{ matrix.platform.target }} + path: target/release/diffenator3-${{ matrix.platform.target }}.tar.gz + + release-pypi: + name: Publish to PyPI + if: startsWith(github.ref, 'refs/tags/v') + needs: [build, build_linux] + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + merge-multiple: true + path: dist + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + + release-github: + permissions: + contents: write + name: Publish to GitHub releases + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + needs: [build, build_linux] + steps: + - uses: actions/download-artifact@v4 + with: + pattern: binaries-* + merge-multiple: true + + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + merge-multiple: true + path: wheels + + - name: Generate requirements.txt with SHA256 hashes + run: | + pipx install pip-tools + pipx runpip pip-tools install 'pip==25.0.1' + echo diffenator3 | pip-compile - --no-index --find-links wheels/ --no-emit-find-links --generate-hashes --pip-args '--only-binary=:all:' --no-annotate --no-header --output-file requirements.txt + + - name: Compute checksums of release assets + run: | + sha256sum *.tar.gz *.zip requirements.txt > checksums.txt + + - name: Detect if tag is a pre-release + id: before_release + env: + PRERELEASE_TAG_PATTERN: "v[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+([ab]|rc)[[:digit:]]+" + run: | + TAG_NAME="${GITHUB_REF##*/}" + if egrep -q "$PRERELEASE_TAG_PATTERN" <<< "$TAG_NAME"; then + echo "Tag ${TAG_NAME} contains a pre-release suffix" + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "Tag ${TAG_NAME} does not contain a pre-release suffix" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + fi + echo "release_title=${TAG_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Release + uses: softprops/action-gh-release@v2 + with: + name: ${{ steps.before_release.outputs.release_title }} + files: | + *.tar.gz + *.zip + checksums.txt + requirements.txt + prerelease: ${{ steps.before_release.outputs.is_prerelease }} + generate_release_notes: true diff --git a/Cargo.lock b/Cargo.lock index 194e5b0..698ac94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -477,12 +477,15 @@ dependencies = [ "colored", "diffenator3-lib", "env_logger", + "fancy-regex", + "google-fonts-languages", "indexmap 1.9.3", "itertools 0.13.0", "rayon", "serde", "serde_json", "skrifa", + "tera", "ttj", ] diff --git a/diff3proof/pyproject.toml b/diff3proof/pyproject.toml deleted file mode 100644 index 9301ebd..0000000 --- a/diff3proof/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[build-system] -requires = ["maturin>=1.0,<2.0"] -build-backend = "maturin" - -[tool.maturin] -# treat the project as a Rust binary application: https://www.maturin.rs/bindings#bin -bindings = "bin" - -[project] -name = "diff3proof" \ No newline at end of file diff --git a/diffenator3-cli/Cargo.toml b/diffenator3-cli/Cargo.toml index 9ffccf3..1fd080f 100644 --- a/diffenator3-cli/Cargo.toml +++ b/diffenator3-cli/Cargo.toml @@ -2,6 +2,18 @@ name = "diffenator3" version = "0.1.0" edition = "2021" +license = "Apache-2.0" +description = "A utility for comparing two font files" +repository = "https://github.com/googlefonts/diffenator3" +readme = "../README.md" + +[[bin]] +name = "diffenator3" +path = "src/main.rs" + +[[bin]] +name = "diff3proof" +path = "src/bin/diff3proof.rs" [dependencies] diffenator3-lib = { path = "../diffenator3-lib", features = ["html"] } @@ -16,3 +28,8 @@ colored = "2.1.0" clap = { version = "4.5.9", features = ["derive"] } itertools = "0.13.0" env_logger = "0.11" + +# diff3proof dependencies +google-fonts-languages = "0" +tera = "1" +fancy-regex = "0.13" diff --git a/diffenator3-cli/pyproject.toml b/diffenator3-cli/pyproject.toml deleted file mode 100644 index 0cad7ba..0000000 --- a/diffenator3-cli/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[build-system] -requires = ["maturin>=1.0,<2.0"] -build-backend = "maturin" - -[tool.maturin] -# treat the project as a Rust binary application: https://www.maturin.rs/bindings#bin -bindings = "bin" - -[project] -name = "diffenator3" \ No newline at end of file diff --git a/diffenator3-cli/src/bin/diff3proof.rs b/diffenator3-cli/src/bin/diff3proof.rs new file mode 100644 index 0000000..a1374b7 --- /dev/null +++ b/diffenator3-cli/src/bin/diff3proof.rs @@ -0,0 +1,213 @@ +use std::collections::HashMap; +use std::path::Path; +use std::{collections::HashSet, path::PathBuf}; + +/// Create before/after HTML proofs of two fonts +// In a way this is not related to the core goal of diffenator3, but +// at the same time, we happen to have all the moving parts required +// to make this, and it would be a shame not to use them. +use clap::Parser; +use diffenator3_lib::dfont::{shared_axes, DFont}; +use diffenator3_lib::html::{gen_html, template_engine}; +use env_logger::Env; +use google_fonts_languages::{SampleTextProto, LANGUAGES, SCRIPTS}; +use serde_json::json; + +#[derive(Parser, Debug, clap::ValueEnum, Clone, PartialEq)] +enum SampleMode { + /// Sample text emphasises real language input + Context, + /// Sample text optimizes for codepoint coverage + Cover, +} + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Cli { + /// Output directory for HTML + #[clap(long = "output", default_value = "out")] + output: String, + + /// Directory for custom templates + #[clap(long = "templates")] + templates: Option, + + /// Update diffenator3's stock templates + #[clap(long = "update-templates")] + update_templates: bool, + + /// Point size for sample text in pixels + #[clap(long = "point-size", default_value = "25")] + point_size: u32, + + /// Choice of sample text + #[clap(long = "sample-mode", default_value = "context")] + sample_mode: SampleMode, + + /// Update + /// The first font file to compare + font1: PathBuf, + /// The second font file to compare + font2: Option, +} + +fn main() { + let cli = Cli::parse(); + env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init(); + + let font_binary_a = std::fs::read(&cli.font1).expect("Couldn't open file"); + + let tera = template_engine(cli.templates.as_ref(), cli.update_templates); + let font_a = DFont::new(&font_binary_a); + + let (shared_codepoints, axes, instances) = if let Some(font2) = &cli.font2 { + let font_binary_b = std::fs::read(&font2).expect("Couldn't open file"); + let font_b = DFont::new(&font_binary_b); + + let shared_codepoints: HashSet = font_a + .codepoints + .intersection(&font_b.codepoints) + .copied() + .collect(); + let (axes, instances) = shared_axes(&font_a, &font_b); + (shared_codepoints, axes, instances) + } else { + let shared_codepoints = font_a.codepoints.clone(); + let (axes, instances) = shared_axes(&font_a, &font_a); + (shared_codepoints, axes, instances) + }; + + let axes_instances = serde_json::to_string(&json!({ + "axes": axes, + "instances": instances + })) + .unwrap(); + + let mut variables = serde_json::Map::new(); + variables.insert("axes_instances".to_string(), axes_instances.into()); + match cli.sample_mode { + SampleMode::Context => { + let sample_texts = language_sample_texts(&shared_codepoints); + variables.insert("language_samples".to_string(), json!(sample_texts)); + } + SampleMode::Cover => { + let sample_text = cover_sample_texts(&shared_codepoints); + variables.insert("cover_sample".to_string(), json!(sample_text)); + } + } + + gen_html( + &cli.font1, + &cli.font2.unwrap_or_else(|| cli.font1.clone()), + Path::new(&cli.output), + tera, + "diff3proof.html", + &variables.into(), + "diff3proof.html", + cli.point_size, + ); +} + +fn longest_sampletext(st: &SampleTextProto) -> &str { + if let Some(text) = &st.specimen_16 { + return text; + } + if let Some(text) = &st.specimen_21 { + return text; + } + if let Some(text) = &st.specimen_32 { + return text; + } + if let Some(text) = &st.specimen_36 { + return text; + } + if let Some(text) = &st.specimen_48 { + return text; + } + if let Some(text) = &st.tester { + return text; + } + "" +} + +fn language_sample_texts(codepoints: &HashSet) -> HashMap> { + let mut texts = HashMap::new(); + let re = fancy_regex::Regex::new(r"^(.{20,})(\1)").unwrap(); + let mut seen_cps = HashSet::new(); + // Sort languages by number of speakers + let mut languages: Vec<_> = LANGUAGES.values().collect(); + languages.sort_by_key(|lang| -lang.population.unwrap_or(0)); + + for lang in languages.iter() { + if let Some(sample) = lang.sample_text.as_ref().map(longest_sampletext) { + let mut sample = sample.replace('\n', " "); + let sample_chars = sample.chars().map(|c| c as u32).collect::>(); + + // Can we render this text? + if !sample_chars.is_subset(codepoints) { + continue; + } + // Does this add anything new to the mix? + if sample_chars.is_subset(&seen_cps) { + continue; + } + seen_cps.extend(sample_chars); + let script = lang.script(); + let script_name = SCRIPTS.get(script).unwrap().name(); + // Remove repeated phrases + if let Ok(Some(captures)) = re.captures(&sample) { + sample = captures.get(1).unwrap().as_str().to_string(); + } + texts + .entry(script_name.to_string()) + .or_insert_with(Vec::new) + .push((lang.name().to_string(), sample.to_string())); + } + } + texts +} + +fn cover_sample_texts(codepoints: &HashSet) -> String { + // Create a bag of shapable words + let mut words = HashSet::new(); + let mut languages: Vec<_> = LANGUAGES.values().collect(); + languages.sort_by_key(|lang| -lang.population.unwrap_or(0)); + + for lang in languages.iter() { + if let Some(sample) = lang.sample_text.as_ref().map(longest_sampletext) { + let sample = sample.replace('\n', " "); + for a_word in sample.split_whitespace() { + let word_chars = a_word.chars().map(|c| c as u32).collect::>(); + // Can we render this text? + if !word_chars.is_subset(codepoints) { + continue; + } + words.insert(a_word.to_string()); + } + } + } + + // Now do the greedy cover + let mut uncovered_codepoints = codepoints.clone(); + let mut best_words = vec![]; + let mut prev_count = usize::MAX; + while !uncovered_codepoints.is_empty() { + if uncovered_codepoints.len() == prev_count { + break; + } + prev_count = uncovered_codepoints.len(); + let best_word = words + .iter() + .max_by_key(|word| { + let word_chars = word.chars().map(|c| c as u32).collect::>(); + word_chars.intersection(&uncovered_codepoints).count() + }) + .unwrap(); + for char in best_word.chars() { + uncovered_codepoints.remove(&(char as u32)); + } + best_words.push(best_word.to_string()); + } + best_words.sort(); + best_words.join(" ") +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ade9960 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[tool.maturin] +manifest-path = "diffenator3-cli/Cargo.toml" +bindings = "bin" +include = [ + { path = "README.md", format = "sdist" }, + { path = "LICENSE.md", format = "sdist" }, +] +exclude = [ + { path = "diffenator3-cli/**/*", format = "wheel" }, +] + +[project] +name = "diffenator3" +description = "A utility for comparing two font files" +dynamic = ["version"] +readme = "README.md" From 12b2181c92b43e0b34129c634cd2845c1699e2f8 Mon Sep 17 00:00:00 2001 From: Marc Foley Date: Fri, 23 Jan 2026 15:25:45 +0000 Subject: [PATCH 6/6] Fix lifetime elision warning in fontref() --- diffenator3-lib/src/dfont.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diffenator3-lib/src/dfont.rs b/diffenator3-lib/src/dfont.rs index 90ec236..cfa0dfe 100644 --- a/diffenator3-lib/src/dfont.rs +++ b/diffenator3-lib/src/dfont.rs @@ -91,7 +91,7 @@ impl DFont { Ok(()) } - pub fn fontref(&self) -> FontRef { + pub fn fontref(&self) -> FontRef<'_> { FontRef::new(&self.backing).expect("Couldn't parse font") } pub fn family_name(&self) -> String {