diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 24bec34c..36dffc08 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -17,8 +17,6 @@ jobs: run: devcontainer up --workspace-folder . - name: Update generated files run: devcontainer exec --workspace-folder . make --always-make check_generated_files check_generated_files_container - - name: Update generated without acap-build - run: devcontainer exec --remote-env ACAP_BUILD_IMPL=equivalent --workspace-folder . make check_generated_files_container - name: Run other checks run: devcontainer exec --workspace-folder . make check_other - name: Run miri check diff --git a/.github/workflows/on-host-workflow.yml b/.github/workflows/on-host-workflow.yml index d0ac35df..9660ff0d 100644 --- a/.github/workflows/on-host-workflow.yml +++ b/.github/workflows/on-host-workflow.yml @@ -49,16 +49,14 @@ jobs: - name: Shell setup # This step typically needs to be done once per shell session. # To verify that the procedure has worked, some checks are run as well. - # TODO: Simplify shell session setup; exporting two variables seems excessive run: | export ACAP_SDK_LOCATION=$HOME/axis/ source ./init_env.sh - export ACAP_BUILD_IMPL=equivalent make check_generated_files debian_and_ubuntu_no_sdk: runs-on: ubuntu-24.04 - # If the sysroots from the ACAP Native SDK are not needed, then the Rust implementation of - # `acap-build` can be used for an easier setup: + # If the sysroots from the ACAP Native SDK are not needed, then the native EAP builder + # (`rs4a-eap`, used by `cargo-acap-build`) can be used for an easier setup: steps: - name: Checkout uses: actions/checkout@v4 @@ -76,6 +74,5 @@ jobs: run: | cargo-acap-build -- --package hello_world env: - ACAP_BUILD_IMPL: equivalent CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CARGO_TARGET_THUMBV7NEON_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc diff --git a/Cargo.lock b/Cargo.lock index a2b2082f..582ad859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,24 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "acap-build" -version = "0.0.0" -dependencies = [ - "anyhow", - "clap", - "dirs", - "env_logger", - "glob", - "log", - "regex", - "semver", - "serde", - "serde_json", - "tempdir", - "tempfile", -] - [[package]] name = "acap-logging" version = "0.1.1" @@ -85,6 +67,20 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.2", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -110,9 +106,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -458,6 +454,21 @@ dependencies = [ "which", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -479,6 +490,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "bounding_box_example" version = "0.0.0" @@ -495,6 +512,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "byteorder" version = "1.5.0" @@ -532,13 +555,13 @@ dependencies = [ name = "cargo-acap-build" version = "0.1.0" dependencies = [ - "acap-build", "anyhow", "clap", "cli-version", "dirs", "env_logger", "log", + "rs4a-eap", "serde", "serde_json", "tempfile", @@ -559,6 +582,7 @@ dependencies = [ "env_logger", "log", "reqwest", + "rs4a-eap", "tokio", "url", ] @@ -611,9 +635,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.13" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -621,9 +645,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.13" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -642,9 +666,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -654,9 +678,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cli-version" @@ -762,7 +786,7 @@ dependencies = [ "digest", "hex", "md-5", - "rand 0.8.5", + "rand", "sha2", ] @@ -821,6 +845,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "embedded_web_page" version = "0.0.0" @@ -916,6 +949,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -948,6 +992,17 @@ dependencies = [ "miniz_oxide 0.8.5", ] +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" @@ -979,10 +1034,14 @@ dependencies = [ ] [[package]] -name = "fuchsia-cprng" -version = "0.1.1" +name = "fraction" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] [[package]] name = "futures" @@ -1578,6 +1637,30 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161c33c3ec738cfea3288c5c53dfcdb32fd4fc2954de86ea06f71b5a1a40bfcd" +dependencies = [ + "ahash", + "base64 0.22.1", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "idna", + "itoa", + "num-cmp", + "once_cell", + "percent-encoding", + "referencing", + "regex-syntax", + "serde", + "serde_json", + "uuid-simd", +] + [[package]] name = "larod-sys" version = "0.0.0" @@ -1611,7 +1694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -1824,6 +1907,84 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.1" @@ -1845,9 +2006,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl" @@ -1898,6 +2059,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking" version = "2.2.1" @@ -2026,19 +2193,6 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - [[package]] name = "rand" version = "0.8.5" @@ -2047,7 +2201,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -2057,24 +2211,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.6.4" @@ -2084,15 +2223,6 @@ dependencies = [ "getrandom 0.2.15", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -2122,11 +2252,45 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "referencing" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a64b3a635fad9000648b4d8a59c8710c523ab61a23d392a7d91d47683f5adc" +dependencies = [ + "ahash", + "fluent-uri", + "once_cell", + "parking_lot", + "percent-encoding", + "serde_json", +] + [[package]] name = "regex" -version = "1.11.1" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -2136,9 +2300,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2147,18 +2311,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -2248,6 +2403,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rs4a-eap" +version = "0.1.0" +source = "git+https://github.com/apljungquist/rs4acap.git?rev=5a8b7d5627e29608e94da0280c6b25fa1d8aefe5#5a8b7d5627e29608e94da0280c6b25fa1d8aefe5" +dependencies = [ + "anyhow", + "clap", + "flate2", + "jsonschema", + "log", + "regex", + "semver", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2654,16 +2825,6 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" -[[package]] -name = "tempdir" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" -dependencies = [ - "rand 0.4.6", - "remove_dir_all", -] - [[package]] name = "tempfile" version = "3.20.0" @@ -2951,7 +3112,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.8.5", + "rand", "sha1", "thiserror 1.0.69", "url", @@ -2970,7 +3131,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.8.5", + "rand", "sha1", "thiserror 1.0.69", "utf-8", @@ -3046,6 +3207,23 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + [[package]] name = "vapix_access" version = "0.0.0" @@ -3111,6 +3289,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "want" version = "0.3.1" @@ -3223,28 +3407,6 @@ dependencies = [ "rustix 0.38.34", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-registry" version = "0.2.0" @@ -3488,6 +3650,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index a36ea870..7a02a482 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ tokio = "1.38.1" tower-http = "0.5.2" url = "2.5.2" -acap-build = { path = "crates/acap-build" } acap-logging = { path = "crates/acap-logging", default-features = false } acap-ssh-utils = { path = "crates/acap-ssh-utils" } acap-vapix = { path = "crates/acap-vapix" } @@ -65,6 +64,7 @@ licensekey = { path = "crates/licensekey" } licensekey-sys = { path = "crates/licensekey-sys" } mdb = { path = "crates/mdb" } mdb-sys = { path = "crates/mdb-sys" } +rs4a-eap = { git = "https://github.com/apljungquist/rs4acap.git", rev = "5a8b7d5627e29608e94da0280c6b25fa1d8aefe5", features = ["clap"] } vdo = { path = "crates/vdo" } vdo-sys = { path = "crates/vdo-sys" } diff --git a/crates/acap-build/Cargo.toml b/crates/acap-build/Cargo.toml deleted file mode 100644 index a34eb279..00000000 --- a/crates/acap-build/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "acap-build" -version = "0.0.0" -edition = "2021" - -[dependencies] -anyhow = { workspace = true } -clap = { workspace = true, features = ["derive", "env"] } -dirs = { workspace = true } -env_logger = { workspace = true } -glob = { workspace = true } -log = { workspace = true } -semver = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, features = ["preserve_order"] } -tempfile = { workspace = true } -regex = { workspace = true } -tempdir = { workspace = true } diff --git a/crates/acap-build/src/bin/acap-build.rs b/crates/acap-build/src/bin/acap-build.rs deleted file mode 100644 index c79e7f3c..00000000 --- a/crates/acap-build/src/bin/acap-build.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! A drop-in replacement for the acap-build python script -use std::{ - env, - fmt::{Display, Formatter}, - fs, - path::PathBuf, - process::Command, -}; - -use acap_build::{AppBuilder, Architecture}; -use anyhow::Context; -use clap::{Parser, ValueEnum}; -use log::debug; -use tempdir::TempDir; - -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, ValueEnum)] -#[clap(rename_all = "kebab-case")] -enum BuildOption { - #[default] - Make, - NoBuild, -} - -impl Display for BuildOption { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Make => write!(f, "make"), - Self::NoBuild => write!(f, "no-build"), - } - } -} - -#[derive(Clone, Debug, Parser)] -struct Cli { - path: PathBuf, - /// Build tool, if any, to run before packaging. - #[clap(default_value_t, long, short)] - build: BuildOption, - #[clap(long, short)] - manifest: Option, - /// Note: can be used more than once. - #[clap(long, short)] - additional_file: Vec, -} - -fn main() -> anyhow::Result<()> { - env_logger::init(); - - let Cli { - path, - build, - manifest, - additional_file, - } = Cli::parse(); - match build { - BuildOption::Make => assert!(Command::new("make") - .status() - .context("subprocess make failed")? - .success()), - BuildOption::NoBuild => { - debug!("no build"); - } - } - - let arch: Architecture = env::var("OECORE_TARGET_ARCH")?.parse()?; - let manifest = match manifest { - None => path.join("manifest.json"), - Some(m) => path.join(m), - }; - - let staging_dir = TempDir::new_in(&path, "acap-build")?; - let mut builder = AppBuilder::new(true, staging_dir.path(), &manifest, arch)?; - - for name in builder.mandatory_files() { - builder.add(&path.join(name))?; - } - - for name in builder.optional_files() { - let file = path.join(name); - if file.symlink_metadata().is_ok() { - builder.add(&file)?; - } - } - - for additional_file in additional_file { - builder.add(&path.join(additional_file))?; - } - - let eap_file_name = builder.build()?; - fs::copy( - staging_dir.path().join(&eap_file_name), - path.join(&eap_file_name), - )?; - - Ok(()) -} diff --git a/crates/acap-build/src/command_utils.rs b/crates/acap-build/src/command_utils.rs deleted file mode 100644 index 882a3224..00000000 --- a/crates/acap-build/src/command_utils.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::io::{BufRead, BufReader}; - -use anyhow::Context; -use log::debug; - -pub trait RunWith { - fn run_with_processed_stdout( - self, - func: impl FnMut(std::io::Result) -> anyhow::Result<()>, - ) -> anyhow::Result<()>; - fn run_with_logged_stdout(self) -> anyhow::Result<()>; -} - -fn spawn(mut cmd: std::process::Command) -> anyhow::Result { - match cmd.spawn() { - Ok(t) => Ok(t), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - let program = cmd.get_program().to_string_lossy().to_string(); - Err(e).context(format!( - "{program} not found, perhaps it must be installed." - )) - } - Err(e) => Err(e.into()), - } -} - -impl RunWith for std::process::Command { - fn run_with_processed_stdout( - mut self, - mut func: impl FnMut(std::io::Result) -> anyhow::Result<()>, - ) -> anyhow::Result<()> { - self.stdout(std::process::Stdio::piped()); - debug!("Spawning child {self:#?}..."); - let mut child = spawn(self)?; - let stdout = child - .stdout - .take() - .expect("not previously taken by this function"); - - let lines = BufReader::new(stdout).lines(); - for line in lines { - func(line)?; - } - - debug!("Waiting for child..."); - let status = child.wait()?; - if !status.success() { - anyhow::bail!("Child failed: {status}"); - } - Ok(()) - } - fn run_with_logged_stdout(self) -> anyhow::Result<()> { - self.run_with_processed_stdout(|line| { - let line = line?; - if !line.is_empty() { - debug!("Child said {line:?}."); - }; - Ok(()) - }) - } -} diff --git a/crates/acap-build/src/files.rs b/crates/acap-build/src/files.rs deleted file mode 100644 index 9c5c35cb..00000000 --- a/crates/acap-build/src/files.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Files that are parsed or created in the course of creating an EAP. -pub(crate) mod cgi_conf; -pub(crate) mod manifest; -pub(crate) mod package_conf; -pub(crate) mod param_conf; diff --git a/crates/acap-build/src/files/cgi_conf.rs b/crates/acap-build/src/files/cgi_conf.rs deleted file mode 100644 index 8dc08d4a..00000000 --- a/crates/acap-build/src/files/cgi_conf.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Code for populating the `cgi.conf` file -use std::fmt::{Display, Formatter}; - -use log::debug; - -use crate::{ - files::manifest::Manifest, - json_ext, - json_ext::{MapExt, ValueExt}, -}; - -#[derive(Debug)] -enum Entry { - Fast { access: String, name: String }, - Other { access: String, name: String }, -} - -#[derive(Debug)] -pub(crate) struct CgiConf(Vec); - -impl CgiConf { - pub(crate) fn new(manifest: &Manifest) -> anyhow::Result> { - let conf = match manifest.try_find_http_config() { - Ok(v) => v, - Err(json_ext::Error::KeyNotFound(_)) => return Ok(None), - Err(e) => return Err(e.into()), - }; - - let mut entries = Vec::new(); - for obj in conf.iter() { - let obj = obj.try_to_object()?; - - let kind = obj.try_get_str("type")?; - if kind == "directory" { - debug!("Skipping httpConfig of type directory"); - continue; - } - - let name = obj.try_get_str("name")?.trim_start_matches('/').to_string(); - - let access = match obj.try_get_str("access")? { - "admin" => "administrator", - access => access, - } - .to_string(); - - entries.push(match kind { - "fastCgi" => Entry::Fast { access, name }, - _ => Entry::Other { access, name }, - }) - } - Ok(Some(Self(entries))) - } -} - -impl Display for CgiConf { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for cgi in &self.0 { - match &cgi { - Entry::Fast { access, name } => writeln!(f, "{access} /{name} fastCgi")?, - Entry::Other { access, name } => writeln!(f, "{access} /{name}")?, - } - } - Ok(()) - } -} diff --git a/crates/acap-build/src/files/manifest.rs b/crates/acap-build/src/files/manifest.rs deleted file mode 100644 index 34d4c5b7..00000000 --- a/crates/acap-build/src/files/manifest.rs +++ /dev/null @@ -1,132 +0,0 @@ -use anyhow::bail; -use log::debug; -use serde::Serialize; -use serde_json::{ser::PrettyFormatter, Map, Serializer, Value}; - -use crate::{ - json_ext, - json_ext::{MapExt, ValueExt}, - Architecture, -}; - -#[derive(Debug)] -pub(crate) struct Manifest(Value); - -impl Manifest { - pub(crate) fn new(manifest: Value, architecture: Architecture) -> anyhow::Result { - let mut manifest = Self(manifest); - let mut schema_version = manifest - .as_object()? - .try_get_str("schemaVersion")? - .to_string(); - - // Make it valid semver - for _ in 0..(2 - schema_version.chars().filter(|&c| c == '.').count()) { - schema_version.push_str(".0"); - } - let schema_version = semver::Version::parse(&schema_version)?; - if schema_version > semver::Version::new(1, 3, 0) { - let setup = manifest.try_find_setup_mut()?; - if let Some(a) = setup.get("architecture") { - if a != "all" && a != architecture.nickname() { - bail!( - "Architecture in manifest ({a}) is not compatible with built target ({:?})", - architecture - ); - } - } else { - debug!( - "Architecture not set in manifest, using {:?}", - &architecture - ); - setup.insert( - "architecture".to_string(), - Value::String(architecture.nickname().to_string()), - ); - } - } - Ok(manifest) - } - - pub(crate) fn as_object(&self) -> json_ext::Result<&Map> { - self.0.try_to_object() - } - - pub(crate) fn as_object_mut(&mut self) -> json_ext::Result<&mut Map> { - self.0.try_to_object_mut() - } - - // TODO: Consider generalizing this to something like `try_get_as_str(&self, path: &[&str])` - pub(crate) fn try_find_app_name(&self) -> json_ext::Result<&str> { - self.as_object()? - .try_get_object("acapPackageConf")? - .try_get_object("setup")? - .try_get_str("appName") - } - - pub(crate) fn try_find_architecture(&self) -> json_ext::Result<&str> { - self.as_object()? - .try_get_object("acapPackageConf")? - .try_get_object("setup")? - .try_get_str("architecture") - } - - pub(crate) fn try_find_friendly_name(&self) -> json_ext::Result<&str> { - self.as_object()? - .try_get_object("acapPackageConf")? - .try_get_object("setup")? - .try_get_str("friendlyName") - } - - pub(crate) fn try_find_http_config(&self) -> json_ext::Result<&Vec> { - self.as_object()? - .try_get_object("acapPackageConf")? - .try_get_object("configuration")? - .try_get_array("httpConfig") - } - - pub(crate) fn try_find_param_config(&self) -> json_ext::Result<&Vec> { - self.as_object()? - .try_get_object("acapPackageConf")? - .try_get_object("configuration")? - .try_get_array("paramConfig") - } - - pub(crate) fn try_find_post_install_script(&self) -> json_ext::Result<&str> { - self.as_object()? - .try_get_object("acapPackageConf")? - .try_get_object("installation")? - .try_get_str("postInstallScript") - } - - pub(crate) fn try_find_pre_uninstall_script(&self) -> json_ext::Result<&str> { - self.as_object()? - .try_get_object("acapPackageConf")? - .try_get_object("uninstallation")? - .try_get_str("preUninstallScript") - } - - pub(crate) fn try_find_version(&self) -> json_ext::Result<&str> { - self.as_object()? - .try_get_object("acapPackageConf")? - .try_get_object("setup")? - .try_get_str("version") - } - - pub(crate) fn try_find_setup_mut(&mut self) -> json_ext::Result<&mut Map> { - self.as_object_mut()? - .try_get_object_mut("acapPackageConf")? - .try_get_object_mut("setup") - } - - pub(crate) fn try_to_string(&self) -> anyhow::Result { - // This file is included in the EAP, so for as long as we want bit-exact output, we must - // take care to serialize the manifest the same way as the python implementation. - // let mut writer = BufWriter::new(String::new()); - let mut data = Vec::new(); - let mut serializer = - Serializer::with_formatter(&mut data, PrettyFormatter::with_indent(b" ")); - self.0.serialize(&mut serializer)?; - Ok(String::from_utf8(data)?) - } -} diff --git a/crates/acap-build/src/files/package_conf.rs b/crates/acap-build/src/files/package_conf.rs deleted file mode 100644 index 575bd290..00000000 --- a/crates/acap-build/src/files/package_conf.rs +++ /dev/null @@ -1,342 +0,0 @@ -//! Code for populating the `package.conf` file -use std::{ - collections::HashMap, - fmt::{Display, Formatter}, -}; - -use anyhow::{bail, Context}; -use log::debug; -use regex::Regex; -use semver::Version; -use serde_json::{Map, Value}; - -use crate::{files::manifest::Manifest, Architecture}; - -#[derive(Clone, Debug)] -pub(crate) struct PackageConf(HashMap<&'static str, String>); - -impl PackageConf { - pub(crate) fn new( - manifest: &Manifest, - other_files: &[&str], - default_arch: Architecture, - ) -> anyhow::Result { - let mut package_conf = Self(HashMap::new()); - package_conf.set_custom_from_manifest(manifest)?; - package_conf.set_custom_from_other_files(other_files)?; - package_conf.set_defaults(default_arch); - Ok(package_conf) - } - - fn set_custom_from_manifest(&mut self, manifest: &Manifest) -> anyhow::Result<()> { - let parameters: HashMap<_, _> = PARAMETERS - .iter() - .flat_map(|p| p.source.map(|s| (s, p.name))) - .collect(); - - let flat_manifest = flattened(manifest.as_object()?.clone()); - let valid_vendor_url = - Regex::new("(?:(?:http|https)://)?(.+)").expect("Hard-coded regex is valid"); - for (path, value) in flat_manifest { - match path.as_str() { - "acapPackageConf.setup.version" => { - let v = value - .as_str() - .context("acapPackageConf.setup.version is not a string")?; - let v = Version::parse(v)?; - self.0.insert("APPMAJORVERSION", v.major.to_string()); - self.0.insert("APPMINORVERSION", v.minor.to_string()); - self.0.insert("APPMICROVERSION", v.patch.to_string()); - } - "acapPackageConf.setup.vendorUrl" => { - let v = value - .as_str() - .context("acapPackageConf.setup.vendorUrl is not a string")?; - let Some(caps) = valid_vendor_url.captures(v) else { - bail!("Expected vendor url to match regex {:?}", valid_vendor_url) - }; - let domain_name = caps - .get(1) - .expect("Hard coded regex as exactly one capture group") - .as_str(); - - self.0.insert( - "VENDORHOMEPAGELINK", - format!(r#"{domain_name}"#), - ); - } - "acapPackageConf.configuration.httpConfig" => { - let v = value - .as_array() - .context("acapPackageConf.configuration.httpConfig is not an array")?; - if !v.is_empty() { - self.0.insert("HTTPCGIPATHS", "cgi.conf".to_string()); - } - } - path => { - if let Some(name) = parameters.get(path) { - let v = value - .as_str() - .with_context(|| format!("{path} is not a string"))? - .to_string(); - self.0.insert(name, v); - } else { - debug!("{path} skipped, no corresponding parameter in package.conf") - } - } - } - } - Ok(()) - } - - fn set_custom_from_other_files(&mut self, other_files: &[&str]) -> anyhow::Result<()> { - if !other_files.is_empty() { - self.0.insert("OTHERFILES", other_files.join(" ")); - } - Ok(()) - } - - fn set_defaults(&mut self, arch: Architecture) { - // If not set from the manifest, eap-create.sh would try to infer it. - self.0 - .entry("APPTYPE") - .or_insert(arch.nickname().to_string()); - - // If not set from the manifest, eap-create.sh would try to infer it from the exe. - // But we know that it must be set for the manifest to be valid. - // TODO: Fail explicitly if app name is not set. - let app_name = self.0.get("APPNAME").cloned().unwrap_or_default(); - // If not set from the manifest, eap-create.sh would fall back on the app name - self.0.entry("PACKAGENAME").or_insert(app_name); - - for Parameter { name, default, .. } in PARAMETERS { - if let Some(v) = default { - self.0.entry(name).or_insert(v.to_string()); - } - } - } -} - -impl Display for PackageConf { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for Parameter { name, .. } in PARAMETERS { - if let Some(value) = self.0.get(name) { - if name == "VENDORHOMEPAGELINK" { - writeln!(f, r#"{name}='{value}'"#)?; - } else { - writeln!(f, r#"{name}="{value}""#)?; - } - } - } - Ok(()) - } -} - -struct Parameter { - name: &'static str, - source: Option<&'static str>, - default: Option<&'static str>, -} - -// TODO: Consider generating this (semi-) automatically from its sources: -// - conversion.py -// - package-conf-parameters.cfg -const PARAMETERS: [Parameter; 25] = [ - Parameter { - name: "PACKAGENAME", - source: Some("acapPackageConf.setup.friendlyName"), - default: Some(""), - }, - // Not supported. The name of the ACAP will be taken from PACKAGENAME instead - Parameter { - name: "MENUNAME", - source: None, - default: None, - }, - Parameter { - name: "APPTYPE", - source: Some("acapPackageConf.setup.architecture"), - default: Some(""), - }, - Parameter { - name: "APPNAME", - source: Some("acapPackageConf.setup.appName"), - default: Some(""), - }, - Parameter { - name: "APPID", - source: Some("acapPackageConf.setup.appId"), - default: Some(""), - }, - // Not supported by manifest ACAP applications - Parameter { - name: "LICENSENAME", - source: None, - default: Some("Available"), - }, - Parameter { - name: "LICENSEPAGE", - source: Some("acapPackageConf.copyProtection.method"), - default: Some("none"), - }, - // If the application uses a custom licensing solution but still want to be able to use the - // standard list.cgi and WebUI to display license status. - // Use this option to allow the application to hook into the AXIS API. - Parameter { - name: "LICENSE_CHECK_ARGS", - source: Some("acapPackageConf.copyProtection.customOptions"), - default: None, - }, - Parameter { - name: "VENDOR", - source: Some("acapPackageConf.setup.vendor"), - default: Some("-"), - }, - Parameter { - name: "REQEMBDEVVERSION", - source: Some("acapPackageConf.setup.embeddedSdkVersion"), - default: Some("2.0"), - }, - Parameter { - name: "APPMAJORVERSION", - source: None, // Treated specially - default: Some("1"), - }, - Parameter { - name: "APPMINORVERSION", - source: None, // Treated specially - default: Some("0"), - }, - Parameter { - name: "APPMICROVERSION", - source: None, // Treated specially - default: Some("0"), - }, - Parameter { - name: "APPGRP", - source: Some("acapPackageConf.setup.user.group"), - default: Some("sdk"), - }, - Parameter { - name: "APPUSR", - source: Some("acapPackageConf.setup.user.username"), - default: Some("sdk"), - }, - Parameter { - name: "APPOPTS", - source: Some("acapPackageConf.setup.runOptions"), - default: Some(""), - }, - Parameter { - name: "OTHERFILES", - source: None, - default: Some(""), - }, - Parameter { - name: "SETTINGSPAGEFILE", - source: Some("acapPackageConf.configuration.settingPage"), - default: Some(""), - }, - // Special name on the link to the settings page is not supported - Parameter { - name: "SETTINGSPAGETEXT", - source: None, - default: Some(""), - }, - Parameter { - name: "VENDORHOMEPAGELINK", - source: None, // Treated specially - default: Some(""), - }, - // Pre-upgrade scripts are not supported - Parameter { - name: "PREUPGRADESCRIPT", - source: None, - default: Some(""), - }, - Parameter { - name: "POSTINSTALLSCRIPT", - source: Some("acapPackageConf.installation.postInstallScript"), - default: Some(""), - }, - Parameter { - name: "STARTMODE", - source: Some("acapPackageConf.setup.runMode"), - default: Some("never"), - }, - Parameter { - name: "HTTPCGIPATHS", - source: None, // Treated specially - default: Some(""), - }, - // Only supported for pre-installed applications - Parameter { - name: "AUTOSTART", - source: None, - default: None, - }, -]; - -fn flattened(root: Map) -> Vec<(String, Value)> { - let mut output = Vec::new(); - let mut values = vec![(String::new(), Value::Object(root))]; - while let Some((path, value)) = values.pop() { - match value { - Value::Null => output.push((path, Value::Null)), - Value::Bool(b) => output.push((path, Value::Bool(b))), - Value::Number(n) => output.push((path, Value::Number(n))), - Value::String(s) => output.push((path, Value::String(s.clone()))), - Value::Array(a) => output.push((path, Value::Array(a.clone()))), - Value::Object(o) => { - for (k, v) in o.into_iter().rev() { - debug_assert!(!k.contains('.')); - let mut p = path.clone(); - if !p.is_empty() { - p.push('.'); - } - p.push_str(&k.to_string()); - values.push((p, v)); - } - } - } - } - output -} - -#[cfg(test)] -mod tests { - use serde_json::{json, Value}; - - use super::*; - - #[test] - fn stringify_works_on_example() { - let value = json!( - { - "a": 1, - "b": [2, 3], - "c": [{"i":4}], - "d": {"j": 5}, - "e": { - "k": 6, - "l": {"x": 7}, - "m": 8, - }, - } - ); - let Value::Object(object) = value else { - panic!("expected object"); - }; - let actual = flattened(object); - let expected = vec![ - ("a".to_string(), json!(1)), - ("b".to_string(), json!([2, 3])), - ("c".to_string(), json!([{"i": 4}])), - ("d.j".to_string(), json!(5)), - ("e.k".to_string(), json!(6)), - ("e.l.x".to_string(), json!(7)), - ("e.m".to_string(), json!(8)), - ]; - assert_eq!(actual, expected); - } -} diff --git a/crates/acap-build/src/files/param_conf.rs b/crates/acap-build/src/files/param_conf.rs deleted file mode 100644 index df5349a0..00000000 --- a/crates/acap-build/src/files/param_conf.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Code for populating the `param.conf` file -use std::fmt::{Display, Formatter}; - -use crate::{ - files::manifest::Manifest, - json_ext, - json_ext::{MapExt, ValueExt}, -}; - -#[derive(Debug)] -enum Entry { - Typed { - name: String, - default: String, - kind: String, - }, - Untyped { - name: String, - default: String, - }, -} - -#[derive(Debug)] -pub(crate) struct ParamConf(Vec); - -impl ParamConf { - pub(crate) fn new(manifest: &Manifest) -> anyhow::Result> { - let param_config = match manifest.try_find_param_config() { - Ok(v) => v, - Err(json_ext::Error::KeyNotFound(_)) => return Ok(None), - Err(e) => return Err(e.into()), - }; - - let mut entries = Vec::new(); - for obj in param_config.iter() { - let obj = obj.try_to_object()?; - let name = obj.try_get_str("name")?.to_string(); - let default = obj.try_get_str("default")?.to_string(); - let kind = obj.try_get_str("type")?.to_string(); - - entries.push(match kind.is_empty() { - false => Entry::Typed { - name, - default, - kind, - }, - true => Entry::Untyped { name, default }, - }) - } - Ok(Some(Self(entries))) - } -} - -impl Display for ParamConf { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for param in &self.0 { - match param { - Entry::Typed { - name, - default, - kind, - } => writeln!(f, r#"{name}="{default}" type="{kind}""#)?, - Entry::Untyped { name, default } => writeln!(f, "{name}={default}")?, - } - } - Ok(()) - } -} diff --git a/crates/acap-build/src/json_ext.rs b/crates/acap-build/src/json_ext.rs deleted file mode 100644 index 93d9a57a..00000000 --- a/crates/acap-build/src/json_ext.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Additional methods for [`serde_json`] types. -use std::{ - any::type_name_of_val, - fmt::{Debug, Display, Formatter}, -}; - -use serde_json::{Map, Value}; - -#[derive(Debug)] -pub(crate) enum Error { - KeyNotFound(&'static str), - WrongType { - key: &'static str, - expected: &'static str, - found: &'static str, - }, -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Error::KeyNotFound(key) => { - write!(f, "Expected key {key}") - } - Error::WrongType { - key, - expected, - found, - } => { - write!(f, "Expected {key} to be {expected}, but found {found}") - } - } - } -} - -impl std::error::Error for Error {} - -pub(crate) type Result = core::result::Result; -pub(crate) trait MapExt { - fn to_object(&self) -> &Map; - fn to_object_mut(&mut self) -> &mut Map; - - fn try_get_array(&self, key: &'static str) -> Result<&Vec> { - match self.to_object().get(key) { - Some(Value::Array(a)) => Ok(a), - Some(v) => Err(Error::WrongType { - key, - expected: "array", - found: type_name_of_val(v), - }), - None => Err(Error::KeyNotFound(key)), - } - } - - fn try_get_object(&self, key: &'static str) -> Result<&Map> { - match self.to_object().get(key) { - Some(Value::Object(o)) => Ok(o), - Some(v) => Err(Error::WrongType { - key, - expected: "object", - found: type_name_of_val(v), - }), - None => Err(Error::KeyNotFound(key)), - } - } - - fn try_get_object_mut(&mut self, key: &'static str) -> Result<&mut Map> { - match self.to_object_mut().get_mut(key) { - Some(Value::Object(o)) => Ok(o), - Some(v) => Err(Error::WrongType { - key, - expected: "object", - found: type_name_of_val(v), - }), - None => Err(Error::KeyNotFound(key)), - } - } - - fn try_get_str(&self, key: &'static str) -> Result<&str> { - match self.to_object().get(key) { - Some(Value::String(s)) => Ok(s), - Some(v) => Err(Error::WrongType { - key, - expected: "string", - found: type_name_of_val(v), - }), - None => Err(Error::KeyNotFound(key)), - } - } -} - -impl MapExt for Map { - fn to_object(&self) -> &Map { - self - } - fn to_object_mut(&mut self) -> &mut Map { - self - } -} - -pub(crate) trait ValueExt { - fn try_to_object(&self) -> Result<&Map>; - fn try_to_object_mut(&mut self) -> Result<&mut Map>; -} - -impl ValueExt for Value { - fn try_to_object(&self) -> Result<&Map> { - match self { - Value::Object(o) => Ok(o), - v => Err(Error::WrongType { - key: "?", - expected: "object", - found: type_name_of_val(v), - }), - } - } - fn try_to_object_mut(&mut self) -> Result<&mut Map> { - match self { - Value::Object(o) => Ok(o), - v => Err(Error::WrongType { - key: "?", - expected: "object", - found: type_name_of_val(v), - }), - } - } -} diff --git a/crates/acap-build/src/lib.rs b/crates/acap-build/src/lib.rs deleted file mode 100644 index 16c7f387..00000000 --- a/crates/acap-build/src/lib.rs +++ /dev/null @@ -1,477 +0,0 @@ -#![forbid(unsafe_code)] -//! Library for creating Embedded Application Packages (EAPs). -use std::{ - collections::HashSet, - env, - ffi::OsString, - fs, - io::Write, - os::unix::fs::PermissionsExt, - path::{Path, PathBuf}, - process::Command, - str::FromStr, -}; - -use anyhow::{anyhow, bail, Context}; -use command_utils::RunWith; -use log::{debug, info}; -use semver::Version; -use serde_json::Value; - -use crate::files::{ - cgi_conf::CgiConf, manifest::Manifest, package_conf::PackageConf, param_conf::ParamConf, -}; - -mod command_utils; -mod json_ext; - -mod files; - -// TODO: Find a better way to support reproducible builds -fn copy, Q: AsRef>( - src: P, - dst: Q, - copy_permissions: bool, -) -> anyhow::Result<()> { - let src = src.as_ref(); - let dst = dst.as_ref(); - if dst.symlink_metadata().is_ok() { - bail!("Path already exists {dst:?}"); - } - if src.is_symlink() { - // FIXME: Copy symlink in Rust - let mut cp = Command::new("cp"); - - if copy_permissions { - cp.arg("--preserve=mode"); - } - - cp.arg("-dn").arg(src.as_os_str()).arg(dst.as_os_str()); - - if !cp.status()?.success() { - bail!("Failed to copy symlink: {}", src.display()); - } - } else if copy_permissions { - fs::copy(src, dst)?; - } else { - let mut src = fs::File::open(src)?; - let mut dst = fs::File::create(dst)?; - std::io::copy(&mut src, &mut dst)?; - } - Ok(()) -} - -fn copy_recursively(src: &Path, dst: &Path, copy_permissions: bool) -> anyhow::Result<()> { - if !src.is_dir() { - copy(src, dst, copy_permissions)?; - debug!("Created reg {dst:?}"); - return Ok(()); - } - match fs::create_dir(dst) { - Ok(()) => { - debug!("Created dir {dst:?}"); - Ok(()) - } - Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), - Err(e) => Err(e), - }?; - for entry in fs::read_dir(src)? { - let entry = entry?; - copy_recursively( - &entry.path(), - &dst.join(entry.file_name()), - copy_permissions, - )?; - } - Ok(()) -} - -enum AcapBuildImpl { - Reference, - Equivalent, -} - -impl AcapBuildImpl { - fn from_env_or_default() -> anyhow::Result { - Ok(match env::var_os("ACAP_BUILD_IMPL") { - Some(v) if v.to_string_lossy() == "reference" => Self::Reference, - Some(v) if v.to_string_lossy() == "equivalent" => Self::Equivalent, - Some(v) => { - bail!("Expected ACAP_BUILD_IMPL to be 'reference' or 'equivalent', but found {v:?}") - } - None => Self::Reference, - }) - } -} - -pub struct AppBuilder<'a> { - preserve_permissions: bool, - staging_dir: &'a Path, - manifest: Manifest, - files: Vec, - default_architecture: Architecture, - app_name: String, -} - -impl<'a> AppBuilder<'a> { - pub fn new( - preserve_permissions: bool, - staging_dir: &'a Path, - manifest: &Path, - default_architecture: Architecture, - ) -> anyhow::Result { - let manifest: Value = serde_json::from_reader(fs::File::open(manifest)?)?; - let manifest = Manifest::new(manifest, default_architecture)?; - let app_name = manifest.try_find_app_name()?.to_string(); - Ok(Self { - preserve_permissions, - staging_dir, - manifest, - app_name, - files: Vec::new(), - default_architecture, - }) - } - - /// Add a file to the EAP. - pub fn add(&mut self, path: &Path) -> anyhow::Result<&mut Self> { - let name = path - .file_name() - .context("file has no name")? - .to_str() - .context("file name is not a string")?; - self.add_as(path, name)?; - Ok(self) - } - - /// Add all files in a directory to the EAP. - pub fn add_from(&mut self, dir: &Path) -> anyhow::Result<&mut Self> { - let mut entries = fs::read_dir(dir)? - .map(|res| res.map(|e| e.path())) - .collect::>>()?; - entries.sort(); - for entry in entries { - let name = entry - .file_name() - .context("file has no name")? - .to_str() - .context("file name is not a string")?; - self.add_as(&entry, name)?; - } - Ok(self) - } - - // TODO: Remove the file system copy - pub fn add_as(&mut self, path: &Path, name: &str) -> anyhow::Result { - let dst = self.staging_dir.join(name); - if dst.symlink_metadata().is_ok() { - bail!("Cannot add {path:?} because {name} already exists"); - } - copy_recursively(path, &dst, self.preserve_permissions)?; - self.files.push(name.to_string()); - if name == self.app_name && !self.preserve_permissions { - let mut permissions = fs::metadata(&dst)?.permissions(); - let mode = permissions.mode(); - permissions.set_mode(mode | 0o111); - fs::set_permissions(&dst, permissions)?; - } - debug!("Added {name} from {path:?}"); - Ok(dst) - } - - /// Add the **mandatory** executable to the EAP. - pub fn add_exe(&mut self, reg: &Path) -> anyhow::Result<&mut Self> { - // TODO: Consider refactoring or changing to avoid cloning. - let app_name = self.app_name.clone(); - self.add_as(reg, &app_name)?; - Ok(self) - } - - /// Build the EAP and return its path. - pub fn build(self) -> anyhow::Result { - match AcapBuildImpl::from_env_or_default()? { - AcapBuildImpl::Reference => { - debug!("Using acap-build"); - self.build_foreign() - } - AcapBuildImpl::Equivalent => { - // TODO: Implement validation. - info!("Bypassing acap-build, manifest will not be validated"); - self.build_native() - } - } - } - - fn build_foreign(self) -> anyhow::Result { - let Self { - staging_dir, - default_architecture, - manifest, - .. - } = &self; - - fs::File::create_new(staging_dir.join("manifest.json")) - .context("creating manifest.json")? - .write_all(manifest.try_to_string()?.as_bytes())?; - - let mut acap_build = Command::new("acap-build"); - acap_build.args(["--build", "no-build"]); - for file in self.additional_files() { - acap_build.args(["--additional-file", file]); - } - acap_build.arg("."); - - let mut sh = Command::new("sh"); - sh.current_dir(staging_dir); - - let env_setup = match default_architecture { - Architecture::Aarch64 => "environment-setup-cortexa53-crypto-poky-linux", - Architecture::Armv7hf => "environment-setup-cortexa9hf-neon-poky-linux-gnueabi", - }; - sh.args([ - "-c", - &format!(". /opt/axis/acapsdk/{env_setup} && {acap_build:?}"), - ]); - sh.run_with_logged_stdout()?; - - let mut apps = Vec::new(); - for entry in fs::read_dir(staging_dir)? { - let entry = entry?; - let path = entry.path(); - if let Some(extension) = path.extension() { - if extension.to_str() == Some("eap") { - apps.push(path); - } - } - } - let mut apps = apps.into_iter(); - let app = apps.next().context("Expected at least one artifact")?; - if let Some(second) = apps.next() { - bail!("Built at least one unexpected .eap file {second:?}") - } - Ok(app.file_name().context("file has no name")?.to_os_string()) - } - - fn build_native(self) -> anyhow::Result { - let Self { - staging_dir, - manifest, - - default_architecture, - app_name, - .. - } = &self; - let mtime = match env::var_os("SOURCE_DATE_EPOCH") { - Some(v) => v.into_string().map_err(|e| anyhow!("{e:?}"))?, - None => String::from_utf8(Command::new("date").arg("+%s").output()?.stdout)?, - }; - - // Compute file name - let package_name = match manifest.try_find_friendly_name() { - Ok(v) => v, - Err(json_ext::Error::KeyNotFound(_)) => app_name.as_str(), - Err(e) => return Err(e.into()), - } - .replace(' ', "_"); - let Version { - major, - minor, - patch, - .. - } = manifest.try_find_version().context("no version")?.parse()?; - - let arch = match manifest.try_find_architecture() { - Ok(v) => v, - Err(json_ext::Error::KeyNotFound(_)) => default_architecture.nickname(), - Err(e) => return Err(e.into()), - }; - let eap_file_name = format!("{package_name}_{major}_{minor}_{patch}_{arch}.eap"); - - // Generate derived files - let package_conf = - PackageConf::new(manifest, &self.other_files(), *default_architecture)?.to_string(); - fs::File::create_new(staging_dir.join("package.conf"))? - .write_all(package_conf.as_bytes())?; - - let param_conf = match ParamConf::new(manifest)? { - None => { - // If there is no param.conf, `eap-create.sh` creates one - debug!("Creating empty param.conf"); - String::new() - } - Some(v) => v.to_string(), - }; - fs::File::create_new(staging_dir.join("param.conf"))?.write_all(param_conf.as_bytes())?; - - match CgiConf::new(manifest)? { - None => { - debug!("Skipping cgi.conf") - } - Some(cgi_conf) => { - fs::File::create_new(staging_dir.join("cgi.conf"))? - .write_all(cgi_conf.to_string().as_bytes())?; - } - } - - // This file is included in the EAP, so for as long as we want bit-exact output, we must - // take care to serialize the manifest the same way as the python implementation. - let manifest_file = staging_dir.join("manifest.json"); - fs::File::create_new(&manifest_file)?.write_all(manifest.try_to_string()?.as_bytes())?; - // Replicate the permissions that temporary files get by default. - let mut permissions = fs::metadata(&manifest_file)?.permissions(); - permissions.set_mode(0o600); - fs::set_permissions(&manifest_file, permissions)?; - - // Create the archive - let mut tar = Command::new("tar"); - tar.args(["--exclude", "*~"]) - .args(["--file", &eap_file_name]) - .args(["--format", "gnu"]) - .args(["--group", "0"]) - .args(["--mtime", &format!("@{mtime}")]) - .args(["--owner", "0"]) - .args(["--sort", "name"]) - .args(["--use-compress-program", "gzip --no-name -9"]) - .arg("--create") - .arg("--numeric-owner") - .arg("--exclude-vcs"); - - for name in self.section_1_files() { - if staging_dir.join(name).symlink_metadata().is_ok() { - tar.arg(name); - } - } - - tar.args(self.other_files()); - - // TODO: Consider implementing support for `httpd.conf.local.*` and `mime.types.local.*`. - - for name in self.section_4_files() { - if staging_dir.join(name).symlink_metadata().is_ok() { - tar.arg(name); - } - } - - tar.arg("--verbose"); - tar.current_dir(staging_dir); - tar.run_with_logged_stdout()?; - - Ok(OsString::from(eap_file_name)) - } - - // These sections are probably relevant only for the equivalent and reference implementations; - // Once unpacked on device the order of files or the reason they were included is not important - // (even though some files are nonetheless treated specially). - // The sections don't have any semantics, they are just partitions that can be composed to - // create meaningful or useful lists of names. - - fn section_1_files(&self) -> Vec<&str> { - [ - Some(self.app_name.as_str()), - Some("package.conf"), - Some("param.conf"), - Some("LICENSE"), - Some("manifest.json"), - self.manifest.try_find_post_install_script().ok(), - ] - .into_iter() - .flatten() - .collect() - } - - fn section_2_files(&self) -> Vec<&str> { - let known_files: HashSet<_> = [ - self.section_1_files(), - self.section_3_files(), - self.section_4_files(), - ] - .into_iter() - .flatten() - .collect(); - - self.files - .iter() - .map(String::as_str) - .filter(|f| !known_files.contains(f)) - .collect() - } - - fn section_3_files(&self) -> Vec<&str> { - [self.manifest.try_find_pre_uninstall_script().ok()] - .into_iter() - .flatten() - .collect() - } - - fn section_4_files(&self) -> Vec<&str> { - ["html", "declarations", "lib", "cgi.conf"] - .into_iter() - .collect() - } - - /// Additional files for the reference implementation. - fn additional_files(&self) -> Vec<&str> { - self.section_2_files() - } - - /// Other files for the `package.conf` file. - fn other_files(&self) -> Vec<&str> { - [self.section_2_files(), self.section_3_files()].concat() - } - - /// Return the name of files that must be added using [`Self::add`]. - pub fn mandatory_files(&self) -> Vec { - [ - Some(self.app_name.as_str()), - Some("LICENSE"), - self.manifest.try_find_post_install_script().ok(), - self.manifest.try_find_pre_uninstall_script().ok(), - ] - .into_iter() - .flatten() - .map(str::to_string) - .collect() - } - - /// Return the name of files that should be added using [`Self::add`]. - pub fn optional_files(&self) -> Vec { - ["html", "declarations", "lib"] - .into_iter() - .map(str::to_string) - .collect() - } -} - -#[derive(Clone, Copy, Debug)] -pub enum Architecture { - Aarch64, - Armv7hf, -} - -impl Architecture { - pub fn triple(&self) -> &'static str { - match self { - Architecture::Aarch64 => "aarch64-unknown-linux-gnu", - Architecture::Armv7hf => "thumbv7neon-unknown-linux-gnueabihf", - } - } - - pub fn nickname(&self) -> &'static str { - match self { - Self::Aarch64 => "aarch64", - Self::Armv7hf => "armv7hf", - } - } -} - -impl FromStr for Architecture { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "aarch64" => Ok(Self::Aarch64), - "arm" => Ok(Self::Armv7hf), - _ => Err(anyhow::anyhow!("Unrecognized variant {s}")), - } - } -} diff --git a/crates/cargo-acap-build/Cargo.toml b/crates/cargo-acap-build/Cargo.toml index 24a8e4a6..53a1bf7b 100644 --- a/crates/cargo-acap-build/Cargo.toml +++ b/crates/cargo-acap-build/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] anyhow = { workspace = true } -clap = { workspace = true, features = ["derive"] } +clap = { workspace = true, features = ["derive", "env"] } dirs = { workspace = true } env_logger = { workspace = true, default-features = true } log = { workspace = true } @@ -13,5 +13,5 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tempfile = { workspace = true } -acap-build = { workspace = true } +rs4a-eap = { workspace = true } cli-version = { workspace = true } diff --git a/crates/cargo-acap-build/src/cargo_acap.rs b/crates/cargo-acap-build/src/cargo_acap.rs index 4d2100c0..6393053e 100644 --- a/crates/cargo-acap-build/src/cargo_acap.rs +++ b/crates/cargo-acap-build/src/cargo_acap.rs @@ -1,19 +1,19 @@ -/// This module bridges the gap between `cargo` and `acap-build` using the application structure +/// This module bridges the gap between `cargo` and `rs4a-eap` using the application structure /// conventions detailed in [`crate`]. use std::{ collections::HashMap, path::{Path, PathBuf}, }; -use acap_build::AppBuilder; use anyhow::{bail, Context}; use log::{debug, error, warn}; +use rs4a_eap::{AppBuilder, Mtime}; use crate::{ cargo::{cargo_command, get_cargo_metadata, json_message::JsonMessage}, command_utils::RunWith, files::license, - Architecture, + AcapBuildImpl, Architecture, }; #[derive(Debug)] @@ -21,17 +21,28 @@ pub enum Artifact { Eap { path: PathBuf, name: String }, Exe { path: PathBuf }, } + +/// The Rust target triple used to build for the given architecture. +fn target_triple(arch: Architecture) -> &'static str { + match arch { + Architecture::Aarch64 => "aarch64-unknown-linux-gnu", + Architecture::Armv7hf => "thumbv7neon-unknown-linux-gnueabihf", + } +} + pub fn build_and_pack( arch: Architecture, + acap_build_impl: AcapBuildImpl, args: &[&str], manifest_path: Option<&Path>, + mtime: Mtime, ) -> anyhow::Result> { // If user supplies a target we lose track of which target is currently being built assert!(!args.contains(&"--target")); let mut cargo = cargo_command(manifest_path); cargo.arg("build"); - cargo.args(["--target", arch.triple()]); + cargo.args(["--target", target_triple(arch)]); cargo.args(["--message-format", "json-render-diagnostics"]); @@ -75,9 +86,11 @@ pub fn build_and_pack( path: pack( &cargo_target_directory, arch, + acap_build_impl, manifest_path, executable, out_dir, + mtime, )?, name: target.name, }); @@ -111,11 +124,13 @@ pub fn build_and_pack( fn pack( cargo_target_dir: &Path, arch: Architecture, + acap_build_impl: AcapBuildImpl, manifest_path: PathBuf, executable: PathBuf, out_dir: Option, + mtime: Mtime, ) -> anyhow::Result { - let mut staging_dir = cargo_target_dir.join(arch.nickname()); + let mut staging_dir = cargo_target_dir.join(arch.as_str()); if !staging_dir.is_dir() { std::fs::create_dir(&staging_dir)?; } @@ -138,6 +153,8 @@ fn pack( debug!("Creating app builder"); let mut app_builder = AppBuilder::new(false, &staging_dir, &manifest, arch)?; + app_builder.implementation(acap_build_impl); + app_builder.mtime(mtime); app_builder.add_exe(&executable)?; // TODO: Consider providing defaults for more files. diff --git a/crates/cargo-acap-build/src/lib.rs b/crates/cargo-acap-build/src/lib.rs index 539b2679..aa581bf3 100644 --- a/crates/cargo-acap-build/src/lib.rs +++ b/crates/cargo-acap-build/src/lib.rs @@ -7,10 +7,12 @@ use std::{ path::{Path, PathBuf}, }; -pub use acap_build::Architecture; pub use cargo::get_cargo_metadata; pub use cargo_acap::Artifact; use log::debug; +pub use rs4a_eap::{AcapBuildImpl, Architecture}; +use rs4a_eap::Mtime; + mod cargo; mod cargo_acap; mod command_utils; @@ -21,6 +23,7 @@ pub struct AppBuilder { args: Vec, artifact_dir: Option, manifest_path: Option, + acap_build_impl: AcapBuildImpl, } impl AppBuilder { @@ -34,9 +37,18 @@ impl AppBuilder { args: Vec::new(), artifact_dir: None, manifest_path: None, + acap_build_impl: AcapBuildImpl::Compatible, } } + /// Select the implementation used to package the EAP. + /// + /// Defaults to [`AcapBuildImpl::Compatible`]. + pub fn implementation(&mut self, acap_build_impl: AcapBuildImpl) -> &mut Self { + self.acap_build_impl = acap_build_impl; + self + } + /// Add arguments that will be passed through to cargo. /// /// # Panics @@ -91,12 +103,18 @@ impl AppBuilder { self } - pub fn execute(&mut self) -> anyhow::Result> { + pub fn execute(&mut self, mtime: Mtime) -> anyhow::Result> { let args: Vec<_> = self.args.iter().map(String::as_str).collect(); let manifest_path = self.manifest_path.as_deref(); let mut artifacts = Vec::new(); for target in &self.targets { - artifacts.extend(cargo_acap::build_and_pack(*target, &args, manifest_path)?); + artifacts.extend(cargo_acap::build_and_pack( + *target, + self.acap_build_impl, + &args, + manifest_path, + mtime, + )?); } if let Some(artifact_dir) = self.artifact_dir.as_deref() { copy_final_artifacts(&artifacts, artifact_dir)?; diff --git a/crates/cargo-acap-build/src/main.rs b/crates/cargo-acap-build/src/main.rs index 29fc7bcc..cf9bd1b1 100644 --- a/crates/cargo-acap-build/src/main.rs +++ b/crates/cargo-acap-build/src/main.rs @@ -1,10 +1,11 @@ #![forbid(unsafe_code)] use std::fs::File; -use cargo_acap_build::{get_cargo_metadata, AppBuilder, Architecture}; +use cargo_acap_build::{get_cargo_metadata, AcapBuildImpl, AppBuilder, Architecture}; use clap::{Parser, ValueEnum}; use cli_version::version_with_commit_id; use log::debug; +use rs4a_eap::Mtime; // TODO: Figure out what to call this. // This is sometimes called just "architecture" but in other contexts arch refers to the first @@ -24,6 +25,10 @@ impl From for Architecture { } } +fn parse_mtime(s: &str) -> anyhow::Result { + s.trim().parse::()?.try_into() +} + /// Build app using cargo /// /// Some defaults deviate from Cargo: @@ -37,6 +42,14 @@ struct Cli { /// Can be used multiple times. #[arg(long)] target: Vec, + /// Time to stamp on every archive member, in seconds after the Unix epoch. + /// + /// Defaults to the current time. + #[clap(long, env = "SOURCE_DATE_EPOCH", value_parser = parse_mtime)] + source_date_epoch: Option, + /// Implementation used to package the EAP. + #[clap(long, env = "ACAP_BUILD_IMPL", default_value_t = AcapBuildImpl::Compatible)] + acap_build_impl: AcapBuildImpl, /// Pass additional arguments to `cargo build`. /// /// Beware that not all incompatible arguments have been documented. @@ -67,8 +80,9 @@ fn build_and_copy(cli: Cli) -> anyhow::Result<()> { AppBuilder::from_targets(cli.targets()) .args(args) + .implementation(cli.acap_build_impl) .artifact_dir(get_cargo_metadata(None)?.target_directory.join("acap")) - .execute()?; + .execute(cli.source_date_epoch.unwrap_or_default())?; Ok(()) } diff --git a/crates/cargo-acap-sdk/Cargo.toml b/crates/cargo-acap-sdk/Cargo.toml index 30a9fb76..31611813 100644 --- a/crates/cargo-acap-sdk/Cargo.toml +++ b/crates/cargo-acap-sdk/Cargo.toml @@ -11,6 +11,7 @@ dirs = { workspace = true } env_logger = { workspace = true, default-features = true } log = { workspace = true } reqwest = { workspace = true, default-features = true, features = ["default-tls"] } +rs4a-eap = { workspace = true } tokio = { workspace = true, features = ["full"] } url = { workspace = true } diff --git a/crates/cargo-acap-sdk/src/commands/build_command.rs b/crates/cargo-acap-sdk/src/commands/build_command.rs index baf8ced5..ca6508f9 100644 --- a/crates/cargo-acap-sdk/src/commands/build_command.rs +++ b/crates/cargo-acap-sdk/src/commands/build_command.rs @@ -16,6 +16,8 @@ impl BuildCommand { ResolvedBuildOptions { target, manifest_path, + source_date_epoch, + acap_build_impl, mut args, }, } = self; @@ -32,6 +34,7 @@ impl BuildCommand { let mut builder = AppBuilder::from_targets([Architecture::from(target)]); builder.args(args); + builder.implementation(acap_build_impl); if let Some(ref path) = manifest_path { builder.manifest_path(path); } @@ -41,7 +44,7 @@ impl BuildCommand { .target_directory .join("acap"), ) - .execute()?; + .execute(source_date_epoch.unwrap_or_default())?; Ok(()) } } diff --git a/crates/cargo-acap-sdk/src/commands/install_command.rs b/crates/cargo-acap-sdk/src/commands/install_command.rs index 80a26f78..4af75434 100644 --- a/crates/cargo-acap-sdk/src/commands/install_command.rs +++ b/crates/cargo-acap-sdk/src/commands/install_command.rs @@ -25,6 +25,8 @@ impl InstallCommand { let ResolvedBuildOptions { target, manifest_path, + source_date_epoch, + acap_build_impl, mut args, } = build_options.resolve(&deploy_options).await?; @@ -40,10 +42,11 @@ impl InstallCommand { let mut builder = AppBuilder::from_targets([Architecture::from(target)]); builder.args(args); + builder.implementation(acap_build_impl); if let Some(ref path) = manifest_path { builder.manifest_path(path); } - let artifacts = builder.execute()?; + let artifacts = builder.execute(source_date_epoch.unwrap_or_default())?; // TODO: Handle the case where multiple artifacts of the same kind have the same name. for artifact in artifacts { diff --git a/crates/cargo-acap-sdk/src/commands/run_command.rs b/crates/cargo-acap-sdk/src/commands/run_command.rs index 6f62582d..32c36c02 100644 --- a/crates/cargo-acap-sdk/src/commands/run_command.rs +++ b/crates/cargo-acap-sdk/src/commands/run_command.rs @@ -21,6 +21,8 @@ impl RunCommand { let ResolvedBuildOptions { target, manifest_path, + source_date_epoch, + acap_build_impl, args, } = build_options.resolve(&deploy_options).await?; @@ -36,10 +38,11 @@ impl RunCommand { let mut builder = AppBuilder::from_targets([Architecture::from(target)]); builder.args(args); + builder.implementation(acap_build_impl); if let Some(ref path) = manifest_path { builder.manifest_path(path); } - let artifacts = builder.execute()?; + let artifacts = builder.execute(source_date_epoch.unwrap_or_default())?; for artifact in artifacts { let envs = vec![("RUST_LOG", "debug"), ("RUST_LOG_STYLE", "always")] .into_iter() diff --git a/crates/cargo-acap-sdk/src/commands/test_command.rs b/crates/cargo-acap-sdk/src/commands/test_command.rs index c9c43818..45341408 100644 --- a/crates/cargo-acap-sdk/src/commands/test_command.rs +++ b/crates/cargo-acap-sdk/src/commands/test_command.rs @@ -21,6 +21,8 @@ impl TestCommand { let ResolvedBuildOptions { target, manifest_path, + source_date_epoch, + acap_build_impl, args: mut build_args, } = build_options.resolve(&deploy_options).await?; @@ -38,10 +40,11 @@ impl TestCommand { let mut builder = AppBuilder::from_targets([Architecture::from(target)]); builder.args(build_args); + builder.implementation(acap_build_impl); if let Some(ref path) = manifest_path { builder.manifest_path(path); } - let artifacts = builder.execute()?; + let artifacts = builder.execute(source_date_epoch.unwrap_or_default())?; for artifact in artifacts { debug!("Running {:?}", artifact); diff --git a/crates/cargo-acap-sdk/src/main.rs b/crates/cargo-acap-sdk/src/main.rs index 2b5c6e44..5ddacf91 100644 --- a/crates/cargo-acap-sdk/src/main.rs +++ b/crates/cargo-acap-sdk/src/main.rs @@ -2,10 +2,11 @@ use std::{ffi::OsString, fs::File, path::PathBuf, str::FromStr}; use acap_vapix::{applications_control, basic_device_info, HttpClient}; -use cargo_acap_build::Architecture; +use cargo_acap_build::{AcapBuildImpl, Architecture}; use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; use cli_version::version_with_commit_id; use log::debug; +use rs4a_eap::Mtime; use url::Host; use crate::commands::{ @@ -69,12 +70,24 @@ enum Commands { Completions(CompletionsCommand), } +fn parse_mtime(s: &str) -> anyhow::Result { + s.trim().parse::()?.try_into() +} + // TODO: Include package selection for better completions and help messages. #[derive(clap::Args, Debug, Clone)] struct BuildOptions { /// Path to Cargo.toml. #[arg(long)] manifest_path: Option, + /// Time to stamp on every archive member, in seconds after the Unix epoch. + /// + /// Defaults to the current time. + #[clap(long, env = "SOURCE_DATE_EPOCH", value_parser = parse_mtime)] + source_date_epoch: Option, + /// Implementation used to package the EAP. + #[clap(long, env = "ACAP_BUILD_IMPL", default_value_t = AcapBuildImpl::Compatible)] + acap_build_impl: AcapBuildImpl, /// Pass additional arguments to `cargo build`. /// /// Beware that not all incompatible arguments have been documented. @@ -85,6 +98,8 @@ impl BuildOptions { async fn resolve(self, deploy_options: &DeployOptions) -> anyhow::Result { let Self { manifest_path, + source_date_epoch, + acap_build_impl, args, } = self; // TODO: Consider using `get_properties` instead. @@ -99,6 +114,8 @@ impl BuildOptions { Ok(ResolvedBuildOptions { target, manifest_path, + source_date_epoch, + acap_build_impl, args, }) } @@ -112,6 +129,14 @@ pub struct ResolvedBuildOptions { /// Path to Cargo.toml. #[arg(long)] manifest_path: Option, + /// Time to stamp on every archive member, in seconds after the Unix epoch. + /// + /// Defaults to the current time. + #[clap(long, env = "SOURCE_DATE_EPOCH", value_parser = parse_mtime)] + source_date_epoch: Option, + /// Implementation used to package the EAP. + #[clap(long, env = "ACAP_BUILD_IMPL", default_value_t = AcapBuildImpl::Compatible)] + acap_build_impl: AcapBuildImpl, /// Pass additional arguments to `cargo build`. /// /// Beware that not all incompatible arguments have been documented.