From f59272f204661523456c5c28657a64751431ec16 Mon Sep 17 00:00:00 2001 From: AP Ljungquist Date: Fri, 12 Jul 2024 21:48:57 +0200 Subject: [PATCH] Add cargo-acap-build --- .devcontainer/Dockerfile | 8 +- .devhost/install-venv.sh | 5 + .dockerignore | 1 + .github/workflows/CI.yml | 1 + Cargo.lock | 258 ++++++++++++++++-- Cargo.toml | 2 + Makefile | 89 +++--- README.md | 2 + apps/using_a_build_script/Cargo.toml | 8 + apps/using_a_build_script/build.rs | 99 +++++++ apps/using_a_build_script/build/LICENSE | 21 ++ apps/using_a_build_script/build/index.html | 11 + apps/using_a_build_script/src/main.rs | 45 +++ crates/cargo-acap-build/Cargo.toml | 14 + crates/cargo-acap-build/README.md | 42 +++ crates/cargo-acap-build/src/acap.rs | 171 ++++++++++++ crates/cargo-acap-build/src/acap/manifest.rs | 15 + crates/cargo-acap-build/src/cargo.rs | 16 ++ .../src/cargo/json_message.rs | 31 +++ crates/cargo-acap-build/src/cargo/metadata.rs | 9 + crates/cargo-acap-build/src/cargo_acap.rs | 208 ++++++++++++++ crates/cargo-acap-build/src/command_utils.rs | 77 ++++++ crates/cargo-acap-build/src/lib.rs | 18 ++ crates/cargo-acap-build/src/main.rs | 138 ++++++++++ 24 files changed, 1214 insertions(+), 75 deletions(-) create mode 100644 apps/using_a_build_script/Cargo.toml create mode 100644 apps/using_a_build_script/build.rs create mode 100644 apps/using_a_build_script/build/LICENSE create mode 100644 apps/using_a_build_script/build/index.html create mode 100644 apps/using_a_build_script/src/main.rs create mode 100644 crates/cargo-acap-build/Cargo.toml create mode 100644 crates/cargo-acap-build/README.md create mode 100644 crates/cargo-acap-build/src/acap.rs create mode 100644 crates/cargo-acap-build/src/acap/manifest.rs create mode 100644 crates/cargo-acap-build/src/cargo.rs create mode 100644 crates/cargo-acap-build/src/cargo/json_message.rs create mode 100644 crates/cargo-acap-build/src/cargo/metadata.rs create mode 100644 crates/cargo-acap-build/src/cargo_acap.rs create mode 100644 crates/cargo-acap-build/src/command_utils.rs create mode 100644 crates/cargo-acap-build/src/lib.rs create mode 100644 crates/cargo-acap-build/src/main.rs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 83c31d3..d55bb28 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -56,8 +56,8 @@ RUN rustup show \ ENV PATH=/usr/local/venv/bin:$PATH \ VIRTUAL_ENV=/usr/local/venv -RUN --mount=type=bind,target=/.devhost,source=.devhost \ - cd /.devhost \ +RUN --mount=type=bind,target=/context \ + cd /context/.devhost \ && ./install-venv.sh $VIRTUAL_ENV \ && chmod a+w -R $VIRTUAL_ENV @@ -66,3 +66,7 @@ RUN --mount=type=bind,target=/.devhost,source=.devhost \ # TODO: Replace the example in the README with something that does not mount any volumes. RUN mkdir /.cargo \ && chmod a+w /.cargo/ + +# `install-venv.sh` install a rust package which creates the cargo registry directory as root. +# TODO: Improve this because it is slow (200 seconds). +RUN chmod a+w -R $CARGO_HOME $RUSTUP_HOME diff --git a/.devhost/install-venv.sh b/.devhost/install-venv.sh index fc84512..a8b90c5 100755 --- a/.devhost/install-venv.sh +++ b/.devhost/install-venv.sh @@ -36,6 +36,11 @@ rm /tmp/node-v18.16.1-linux-x64.tar.gz # Install `devcontainer` into venv npm install -g @devcontainers/cli@0.65.0 +# Install `cargo-acap-build` into venv +cargo install --root ${VIRTUAL_ENV} --target-dir /tmp/target --path ../crates/cargo-acap-build + +rm -r /tmp/target + # Create `init_env.sh` in a location where it can be sourced conveniently. if [ ! -z "${INIT_ENV}" ]; then diff --git a/.dockerignore b/.dockerignore index ed95474..00d817e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ * !/.devhost/ +!/crates/cargo-acap-build !/rust-toolchain.toml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7299611..bc64cda 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,3 +19,4 @@ jobs: devcontainer exec --workspace-folder . make --always-make check_all devcontainer exec --workspace-folder . make build AXIS_PACKAGE=licensekey_handler devcontainer exec --workspace-folder . make build AXIS_PACKAGE=embedded_web_page + devcontainer exec --workspace-folder . make build AXIS_PACKAGE=using_a_build_script diff --git a/Cargo.lock b/Cargo.lock index 8a444ec..48a2ae6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,7 +55,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -65,9 +65,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "bindgen" version = "0.69.4" @@ -97,6 +103,20 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "cargo-acap-build" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "dirs", + "env_logger", + "home", + "log", + "serde", + "serde_json", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -133,6 +153,46 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" + [[package]] name = "colorchoice" version = "1.0.0" @@ -158,6 +218,27 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "either" version = "1.11.0" @@ -204,7 +285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -216,6 +297,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "glib-sys" version = "0.19.8" @@ -258,7 +350,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -328,7 +420,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets", + "windows-targets 0.52.0", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", ] [[package]] @@ -437,6 +539,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "pkg-config" version = "0.3.30" @@ -477,6 +585,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.7.2" @@ -510,29 +629,46 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "serde" -version = "1.0.193" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.5" @@ -554,6 +690,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.50" @@ -690,6 +832,14 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "using_a_build_script" +version = "1.0.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -708,6 +858,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "which" version = "4.4.2" @@ -742,13 +898,37 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -757,51 +937,93 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index b6fde8e..9887090 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ glib-sys = "0.19.5" libc = "0.2.153" log = "0.4.20" pkg-config = "0.3.30" +serde = "1.0.204" +serde_json = "1.0.120" syslog = "6.1.1" thiserror = "1.0.57" diff --git a/Makefile b/Makefile index 320ecd3..23af4da 100644 --- a/Makefile +++ b/Makefile @@ -52,9 +52,8 @@ help: @mkhelp print_docs $(firstword $(MAKEFILE_LIST)) help ## Build for -build: target/$(AXIS_DEVICE_ARCH)/$(AXIS_PACKAGE)/_envoy - mkdir -p target/acap - cp $(patsubst %/_envoy,%/*.eap,$^) target/acap +build: + cargo-acap-build --target $(AXIS_DEVICE_ARCH) -- -p $(AXIS_PACKAGE) ## Install on using password and assuming architecture install: @@ -85,11 +84,36 @@ stop: ## ## Prerequisites: ## +## * is recognized by `cargo-acap-build` as an ACAP app. ## * The app is installed on the device. ## * The app is stopped. ## * The device has SSH enabled the ssh user root configured. -run: target/$(AXIS_DEVICE_ARCH)/$(AXIS_PACKAGE)/$(AXIS_PACKAGE) - scp $< root@$(AXIS_DEVICE_IP):/usr/local/packages/$(AXIS_PACKAGE)/$(AXIS_PACKAGE) +## * The device is added to `knownhosts`. +run: + cargo-acap-build --target $(AXIS_DEVICE_ARCH) -- -p $(AXIS_PACKAGE) + scp target/$(AXIS_DEVICE_ARCH)/$(AXIS_PACKAGE)/$(AXIS_PACKAGE) root@$(AXIS_DEVICE_IP):/usr/local/packages/$(AXIS_PACKAGE)/$(AXIS_PACKAGE) + ssh root@$(AXIS_DEVICE_IP) \ + "cd /usr/local/packages/$(AXIS_PACKAGE) && su - acap-$(AXIS_PACKAGE) -s /bin/sh --preserve-environment -c '$(if $(RUST_LOG_STYLE),RUST_LOG_STYLE=$(RUST_LOG_STYLE) )$(if $(RUST_LOG),RUST_LOG=$(RUST_LOG) )./$(AXIS_PACKAGE)'" + +## Build and execute unit tests for on assuming architecture +## +## Forwards the following environment variables to the remote process: +## +## * `RUST_LOG` +## * `RUST_LOG_STYLE` +## +## Prerequisites: +## +## * is recognized by `cargo-acap-build` as an ACAP app. +## * The app is installed on the device. +## * The app is stopped. +## * The device has SSH enabled the ssh user root configured. +## * The device is added to `knownhosts`. +test: + # The `scp` command below needs the wildcard to match exactly one file. + rm -r target/$(AXIS_DEVICE_ARCH)/$(AXIS_PACKAGE)-*/$(AXIS_PACKAGE) ||: + cargo-acap-build --target $(AXIS_DEVICE_ARCH) -- -p $(AXIS_PACKAGE) --tests + scp target/$(AXIS_DEVICE_ARCH)/$(AXIS_PACKAGE)-*/$(AXIS_PACKAGE) root@$(AXIS_DEVICE_IP):/usr/local/packages/$(AXIS_PACKAGE)/$(AXIS_PACKAGE) ssh root@$(AXIS_DEVICE_IP) \ "cd /usr/local/packages/$(AXIS_PACKAGE) && su - acap-$(AXIS_PACKAGE) -s /bin/sh --preserve-environment -c '$(if $(RUST_LOG_STYLE),RUST_LOG_STYLE=$(RUST_LOG_STYLE) )$(if $(RUST_LOG),RUST_LOG=$(RUST_LOG) )./$(AXIS_PACKAGE)'" @@ -101,7 +125,7 @@ check_all: check_build check_docs check_format check_lint check_tests check_gene .PHONY: check_all ## Check that all crates can be built -check_build: target/aarch64/$(AXIS_PACKAGE)/_envoy target/armv7hf/$(AXIS_PACKAGE)/_envoy +check_build: cargo build \ --exclude consume_analytics_metadata \ --exclude licensekey \ @@ -110,8 +134,10 @@ check_build: target/aarch64/$(AXIS_PACKAGE)/_envoy target/armv7hf/$(AXIS_PACKAGE --exclude mdb \ --exclude mdb-sys \ --workspace - cargo build \ - --target aarch64-unknown-linux-gnu \ + cargo-acap-build \ + --target aarch64 \ + -- \ + --exclude cargo-acap-build \ --workspace .PHONY: check_build @@ -196,50 +222,3 @@ fix_lint: crates/%-sys/src/bindings.rs: FORCE cp $(firstword $(wildcard target/*/*/build/$*-sys-*/out/bindings.rs)) $@ - -# Stage the files that will be packaged outside the source tree to avoid -# * cluttering the source tree and `.gitignore` with build artifacts, and -# * having the same file be built for different targets at different times. -# Use the `_envoy` file as a target because -# * `.DELETE_ON_ERROR` does not work for directories, and -# * the name of the `.eap` file is annoying to predict. -# When building for all targets using a single image we cannot rely on wildcard matching. -target/aarch64/$(AXIS_PACKAGE)/_envoy: ENVIRONMENT_SETUP=environment-setup-cortexa53-crypto-poky-linux -target/armv7hf/$(AXIS_PACKAGE)/_envoy: ENVIRONMENT_SETUP=environment-setup-cortexa9hf-neon-poky-linux-gnueabi -target/%/$(AXIS_PACKAGE)/_envoy: AXIS_DEVICE_ARCH=$* -target/%/$(AXIS_PACKAGE)/_envoy: target/%/$(AXIS_PACKAGE)/lib target/%/$(AXIS_PACKAGE)/html target/%/$(AXIS_PACKAGE)/$(AXIS_PACKAGE) target/%/$(AXIS_PACKAGE)/manifest.json target/%/$(AXIS_PACKAGE)/LICENSE - $(ACAP_BUILD) - touch $@ - -target/%/$(AXIS_PACKAGE)/html: FORCE - mkdir -p $(dir $@) - if [ -d $@ ]; then rm -r $@; fi - if [ -d apps/$(AXIS_PACKAGE)/html ]; then cp -r apps/$(AXIS_PACKAGE)/html $@; fi - -target/%/$(AXIS_PACKAGE)/lib: FORCE - mkdir -p $(dir $@) - if [ -d $@ ]; then rm -r $@; fi - if [ -d apps/$(AXIS_PACKAGE)/lib ]; then cp -r apps/$(AXIS_PACKAGE)/lib $@; fi - -target/%/$(AXIS_PACKAGE)/manifest.json: apps/$(AXIS_PACKAGE)/manifest.json - mkdir -p $(dir $@) - cp $< $@ - -target/%/$(AXIS_PACKAGE)/LICENSE: apps/$(AXIS_PACKAGE)/LICENSE - mkdir -p $(dir $@) - cp $< $@ - -# The target triple and the name of the docker image do not match, so -# at some point we need to map one to the other. It might as well be here. -target/aarch64/$(AXIS_PACKAGE)/$(AXIS_PACKAGE): target/aarch64-unknown-linux-gnu/release/$(AXIS_PACKAGE) - mkdir -p $(dir $@) - cp $< $@ - -target/armv7hf/$(AXIS_PACKAGE)/$(AXIS_PACKAGE): target/thumbv7neon-unknown-linux-gnueabihf/release/$(AXIS_PACKAGE) - mkdir -p $(dir $@) - cp $< $@ - -# Always rebuild the executable because configuring accurate cache invalidation is annoying. -target/%/release/$(AXIS_PACKAGE): FORCE - cargo -v build --release --target $* --package $(AXIS_PACKAGE) - touch $@ # This is a hack to make the `_envoy` target above always build diff --git a/README.md b/README.md index bb5d732..30d7324 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ Below is the list of examples available in the repository. : A simple "Hello, World!" application. * [`licensekey_handler`](apps/licensekey_handler/src/main.rs) : An example that illustrates how to check the licensekey status. +* [`using_a_build_script`](apps/using_a_build_script/src/main.rs) +: Uses a build script to generate html, lib and app manifest files at build time. ## Library crates diff --git a/apps/using_a_build_script/Cargo.toml b/apps/using_a_build_script/Cargo.toml new file mode 100644 index 0000000..d271094 --- /dev/null +++ b/apps/using_a_build_script/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "using_a_build_script" +version = "1.0.0" +edition.workspace = true + +[build-dependencies] +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/apps/using_a_build_script/build.rs b/apps/using_a_build_script/build.rs new file mode 100644 index 0000000..26e3f91 --- /dev/null +++ b/apps/using_a_build_script/build.rs @@ -0,0 +1,99 @@ +use std::{env, fs, path, path::Path}; + +use serde_json::json; + +fn generate_additional(out_dir: &Path) { + let additional = out_dir.join("additional-files"); + match fs::create_dir(&additional) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), + Err(e) => Err(e), + } + .unwrap(); + + let bar = additional.join("bar"); + fs::write(bar, "Bravo").unwrap() +} +fn generate_lib(out_dir: &Path) { + let lib = out_dir.join("lib"); + match fs::create_dir(&lib) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), + Err(e) => Err(e), + } + .unwrap(); + + let libfoo = lib.join("libfoo.so"); + fs::write(libfoo, "Foxtrot").unwrap(); +} + +fn generate_license(out_dir: &Path) { + let license_in = "build/LICENSE"; + let license_out = out_dir.join("LICENSE"); + let content = fs::read_to_string(license_in).unwrap(); + println!("cargo:rerun-if-changed={license_in}"); + fs::write(license_out, content).unwrap(); +} + +fn generate_html(out_dir: &Path) { + let html = out_dir.join("html"); + match fs::create_dir(&html) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), + Err(e) => Err(e), + } + .unwrap(); + + let index_in = "build/index.html"; + let index_out = html.join("index.html"); + let content = fs::read_to_string(index_in).unwrap().replace( + "{timestamp}", + &format!( + "{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + ), + ); + println!("cargo:rerun-if-changed={index_in}"); + fs::write(index_out, content).unwrap(); +} + +fn generate_manifest(out_dir: &Path) { + let manifest_out = out_dir.join("manifest.json"); + let content = json!({ + "schemaVersion": "1.2", + "acapPackageConf": { + "setup": { + "appName": "using_a_build_script", + "vendor": "Axis Communications", + "runMode": "never", + "version": "1.0.0" + }, + "configuration": { + "settingPage": "index.html" + } + } + }); + fs::write( + manifest_out, + serde_json::to_string_pretty(&content).unwrap(), + ) + .unwrap(); +} + +fn main() { + let out_dir = path::PathBuf::from(env::var("OUT_DIR").unwrap()); + match fs::create_dir(&out_dir) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), + Err(e) => Err(e), + } + .unwrap(); + generate_additional(&out_dir); + generate_lib(&out_dir); + generate_license(&out_dir); + generate_html(&out_dir); + generate_manifest(&out_dir); +} diff --git a/apps/using_a_build_script/build/LICENSE b/apps/using_a_build_script/build/LICENSE new file mode 100644 index 0000000..38f672f --- /dev/null +++ b/apps/using_a_build_script/build/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Axis Communications AB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/using_a_build_script/build/index.html b/apps/using_a_build_script/build/index.html new file mode 100644 index 0000000..2ab59a9 --- /dev/null +++ b/apps/using_a_build_script/build/index.html @@ -0,0 +1,11 @@ + + + + Dynamic Content + + +

Dynamic Content

+

A simple, web page generated at build time.

+timestamp={timestamp} + + diff --git a/apps/using_a_build_script/src/main.rs b/apps/using_a_build_script/src/main.rs new file mode 100644 index 0000000..0cc3178 --- /dev/null +++ b/apps/using_a_build_script/src/main.rs @@ -0,0 +1,45 @@ +//! A simple example application demonstrating how to use a build script to generate files +//! dynamically. +//! +//! All applications require a program, but for this example it doesn't need to do anything, +//! hence the empty main function. +fn main() {} + +// TODO: Figure out how to resolve paths on host +// It is not particularly interesting for this app, but once the reverse proxy example is added it +// becomes feasible to serve the embedded web page also when testing on host. +#[cfg(not(target_arch = "x86_64"))] +#[cfg(test)] +mod tests { + use std::{env, path::PathBuf}; + + fn package_dir() -> PathBuf { + env::current_exe().unwrap().parent().unwrap().to_path_buf() + } + + fn additional_files_dir() -> PathBuf { + package_dir() + } + fn lib_dir() -> PathBuf { + package_dir().join("lib") + } + + fn html_dir() -> PathBuf { + package_dir().join("html") + } + + #[test] + fn additional_files_are_installed() { + assert!(additional_files_dir().join("bar").is_file()); + } + + #[test] + fn lib_files_are_installed() { + assert!(lib_dir().join("libfoo.so").is_file()) + } + + #[test] + fn html_files_are_installed() { + assert!(html_dir().join("index.html").is_file()) + } +} diff --git a/crates/cargo-acap-build/Cargo.toml b/crates/cargo-acap-build/Cargo.toml new file mode 100644 index 0000000..90cee11 --- /dev/null +++ b/crates/cargo-acap-build/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "cargo-acap-build" +version = "0.0.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.79" +clap = { version = "4.5.1", features = ["derive"] } +dirs = "5.0.1" +env_logger = "0.11.1" +home = "0.5.9" +log = "0.4.17" +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.114" diff --git a/crates/cargo-acap-build/README.md b/crates/cargo-acap-build/README.md new file mode 100644 index 0000000..01d85cb --- /dev/null +++ b/crates/cargo-acap-build/README.md @@ -0,0 +1,42 @@ +# `cargo-acap-build` + +A tool for building ACAP apps and other executables for deployment to Axis devices. + +## Application project structure + +A simple project will look something like: + +- `/`: Directory in which the package manifest is located[^1]. + - `Cargo.toml`: Package manifest[^2] describing the binary crate that will be included in the + app. + - `src/` + - `main.rs`: Conventional location for the executable[^3]. + - `LICENSE`: Open source acknowledgements[^4]. + - `manifest.json` Application manifest defining the app and its configuration[^5]. + If absent, the resulting artifact will be a plain executable instead of an Embedded Application Package (EAP). + - `html/`: Optional web client pages to include in the app[^6][^7]. + - `lib/`: Optional dynamically linked libraries to include in the app[^6][^7]. + - `additional-files/`: Optional additional files to include in the app[^7]. + +Some files and directories may be generated by a build script[^8] and placed in `/`[^9] instead of in `/`: + +- `LICENSE` +- `manifest.json` +- `html/` +- `lib/` +- `additional-files/` + +In addition to the above structure this tool makes the following assumptions: + +- The package name (`package.name` in `Cargo.toml`) matches the app name (`acapPackageConf.setup.appName` in `manifest.json`). +- The package version (`package.version` in `Cargo.toml`) matches the app version (`acapPackageConf.setup.version` in `manifest.json`). + +[^1]: Defined in +[^2]: Documented in +[^3]: A conventional Rust package layout is described in +[^4]: Documented in +[^5]: Documented in +[^6]: Mentioned in +[^7]: Mentioned in +[^8]: Documented in +[^9]: One of many variables set for build scripts: diff --git a/crates/cargo-acap-build/src/acap.rs b/crates/cargo-acap-build/src/acap.rs new file mode 100644 index 0000000..ca0da3c --- /dev/null +++ b/crates/cargo-acap-build/src/acap.rs @@ -0,0 +1,171 @@ +/// Wrapper around the ACAP SDK, in particular`acap-build`. +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{bail, Context}; +use log::debug; + +use crate::command_utils::RunWith; + +mod manifest; + +fn copy_recursively(src: &Path, dst: &Path) -> anyhow::Result<()> { + if src.is_file() { + if dst.exists() { + bail!("Path already exists {dst:?}"); + } + fs::copy(src, dst)?; + debug!("Created reg {dst:?}"); + return Ok(()); + } + if !src.is_dir() { + bail!("`{src:?}` is neither a file nor a directory"); + } + 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()))?; + } + Ok(()) +} + +pub struct AppBuilder { + staging_dir: PathBuf, + arch: Architecture, + additional_files: Vec, +} + +impl AppBuilder { + pub fn new( + staging_dir: PathBuf, + arch: Architecture, + app_name: &str, + manifest: &Path, + exe: &Path, + license: &Path, + ) -> anyhow::Result { + fs::create_dir(&staging_dir)?; + + fs::copy(manifest, staging_dir.join("manifest.json"))?; + fs::copy(exe, staging_dir.join(app_name))?; + fs::copy(license, staging_dir.join("LICENSE"))?; + + Ok(Self { + staging_dir, + arch, + additional_files: Vec::new(), + }) + } + + pub fn additional(&mut self, dir: &Path) -> anyhow::Result<&mut Self> { + let entries = fs::read_dir(dir)?; + for entry in entries { + let entry = entry?; + let src = entry.path(); + let dst = self.staging_dir.join(entry.file_name()); + if dst.exists() { + bail!("{} already exists", entry.file_name().to_string_lossy()); + } + copy_recursively(&src, &dst)?; + self.additional_files + .push(src.strip_prefix(dir)?.to_path_buf()); + } + Ok(self) + } + pub fn lib(&mut self, dir: &Path) -> anyhow::Result<&mut Self> { + let name = "lib"; + let dst = self.staging_dir.join(name); + if dst.exists() { + bail!("{name} already exists"); + } + copy_recursively(dir, &dst)?; + Ok(self) + } + + pub fn html(&mut self, dir: &Path) -> anyhow::Result<&mut Self> { + let name = "html"; + let dst = self.staging_dir.join(name); + if dst.exists() { + bail!("{name} already exists"); + } + copy_recursively(dir, &dst)?; + Ok(self) + } + + pub fn build(&self) -> anyhow::Result { + let Self { + staging_dir, + additional_files, + .. + } = self; + + let mut acap_build = std::process::Command::new("acap-build"); + acap_build.args(["--build", "no-build"]); + for file in additional_files { + // Use `arg` twice to avoid fallible conversion from `&PathBuf` to `&str`. + acap_build.arg("--additional-file"); + acap_build.arg(file); + } + acap_build.arg("."); + + let mut sh = std::process::Command::new("sh"); + sh.current_dir(staging_dir); + + let env_setup = match self.arch { + 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) + } +} + +#[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", + } + } +} diff --git a/crates/cargo-acap-build/src/acap/manifest.rs b/crates/cargo-acap-build/src/acap/manifest.rs new file mode 100644 index 0000000..a8eda34 --- /dev/null +++ b/crates/cargo-acap-build/src/acap/manifest.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Manifest { + pub acap_package_conf: AcapPackageConf, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AcapPackageConf { + pub setup: Setup, +} +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Setup { + pub app_name: String, +} diff --git a/crates/cargo-acap-build/src/cargo.rs b/crates/cargo-acap-build/src/cargo.rs new file mode 100644 index 0000000..93a3117 --- /dev/null +++ b/crates/cargo-acap-build/src/cargo.rs @@ -0,0 +1,16 @@ +/// Wrapper around `cargo`. +use metadata::CargoMetadata; + +use crate::command_utils::RunWith; + +pub mod json_message; +pub mod metadata; + +pub fn get_cargo_metadata() -> anyhow::Result { + let mut cargo = std::process::Command::new("cargo"); + cargo.arg("metadata"); + cargo.args(["--format-version", "1"]); + let metadata = cargo.run_with_captured_stdout()?; + let metadata: CargoMetadata = serde_json::from_str(&metadata)?; + Ok(metadata) +} diff --git a/crates/cargo-acap-build/src/cargo/json_message.rs b/crates/cargo-acap-build/src/cargo/json_message.rs new file mode 100644 index 0000000..a955ae5 --- /dev/null +++ b/crates/cargo-acap-build/src/cargo/json_message.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(tag = "reason")] +pub enum JsonMessage { + #[serde(rename = "compiler-artifact")] + CompilerArtifact { + package_id: String, + manifest_path: PathBuf, + executable: Option, + target: Target, + }, + // We don't care about the content of these for now, but include them so that the parsing + // succeeds. + #[serde(rename = "compiler-message")] + CompilerMessage { message: String }, + #[serde(rename = "build-script-executed")] + BuildScriptExecuted { + package_id: String, + out_dir: PathBuf, + }, + #[serde(rename = "build-finished")] + BuildFinished { success: bool }, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Target { + pub name: String, +} diff --git a/crates/cargo-acap-build/src/cargo/metadata.rs b/crates/cargo-acap-build/src/cargo/metadata.rs new file mode 100644 index 0000000..502c9b8 --- /dev/null +++ b/crates/cargo-acap-build/src/cargo/metadata.rs @@ -0,0 +1,9 @@ +use std::path::PathBuf; + +use serde::Deserialize; + +#[derive(Clone, Debug, Deserialize)] +pub struct CargoMetadata { + pub target_directory: PathBuf, + pub workspace_root: PathBuf, +} diff --git a/crates/cargo-acap-build/src/cargo_acap.rs b/crates/cargo-acap-build/src/cargo_acap.rs new file mode 100644 index 0000000..ee46578 --- /dev/null +++ b/crates/cargo-acap-build/src/cargo_acap.rs @@ -0,0 +1,208 @@ +/// This module bridges the gap between `cargo` and `acap-build` using the application structure +/// conventions detailed in [`crate`]. +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use anyhow::{bail, Context}; +use log::{debug, error, warn}; + +use crate::{ + acap::{AppBuilder, Architecture}, + cargo::{get_cargo_metadata, json_message::JsonMessage}, + command_utils::RunWith, +}; + +pub fn build_and_pack(arch: Architecture, args: &[&str]) -> anyhow::Result> { + // If user supplies a target we lose track of which target is currently being built + assert!(!args.contains(&"--target")); + + let mut cargo = std::process::Command::new("cargo"); + cargo.arg("build"); + cargo.args(["--target", arch.triple()]); + + cargo.args(["--message-format", "json-render-diagnostics"]); + + // Allow the user to customize the behaviour in unanticipated or not-yet-supported ways. + cargo.args(args); + + let mut messages = Vec::new(); + cargo.run_with_processed_stdout(|line| { + match line { + Ok(line) => match serde_json::from_str::(&line) { + Ok(message) => messages.push(message), + Err(e) => error!("Could not parse line because {e}"), + }, + Err(e) => { + error!("Could not take line because {e}"); + return Ok(()); + } + } + Ok(()) + })?; + + let cargo_target_directory = get_cargo_metadata()?.target_directory; + let mut out_dirs = HashMap::new(); + let mut artifacts = Vec::new(); + for m in messages { + match m { + JsonMessage::CompilerArtifact { + package_id, + manifest_path, + executable, + target, + } => { + let Some(executable) = executable else { + debug!("Artifact is not an executable, skipping {package_id}"); + continue; + }; + let out_dir = out_dirs.get(&package_id).cloned(); + if is_app(&manifest_path, out_dir.as_deref()) { + // If the executable should be an ACAP app, create an `.eap` file. + artifacts.push(pack( + &cargo_target_directory, + arch, + target.name, + manifest_path, + executable, + out_dir, + )?); + } else { + // If the executable should not be an ACAP app, leave it as is. + artifacts.push(executable); + } + } + JsonMessage::CompilerMessage { message } => { + // We expect these to be rendered to stderr when `--message-format` is + // set to `json-render-diagnostics`, as opposed to `json`. + error!("Received compiler-message: {message}") + } + JsonMessage::BuildFinished { success } => { + debug!("Received build-finished message (success: {success})") + } + JsonMessage::BuildScriptExecuted { + package_id, + out_dir, + } => { + debug!("Received build-script-executed message for {package_id}"); + if let Some(out_dir) = out_dirs.insert(package_id, out_dir) { + warn!("Discarding out dir {out_dir:?}") + } + } + } + } + Ok(artifacts) +} + +fn pack( + cargo_target_dir: &Path, + arch: Architecture, + package_name: String, + manifest_path: PathBuf, + executable: PathBuf, + out_dir: Option, +) -> anyhow::Result { + let mut staging_dir = cargo_target_dir.join(arch.nickname()); + if !staging_dir.is_dir() { + std::fs::create_dir(&staging_dir)?; + } + staging_dir.push( + executable + .file_name() + .context("built exe has no file name")?, + ); + if staging_dir.is_dir() { + std::fs::remove_dir_all(&staging_dir)?; + } + + let manifest_dir = manifest_path + .parent() + .context("cargo manifest has no parent")?; + + let manifest = exactly_one(manifest_dir, out_dir.as_deref(), "manifest.json")?; + debug!("Found manifest file: {manifest:?}"); + let license = exactly_one(manifest_dir, out_dir.as_deref(), "LICENSE")?; + debug!("Found license file: {license:?}"); + + debug!("Creating app builder"); + let mut app_builder = AppBuilder::new( + staging_dir, + arch, + &package_name, + &manifest, + &executable, + &license, + )?; + + if let Some(d) = at_most_one(manifest_dir, out_dir.as_deref(), "additional-files")? { + debug!("Found additional-files dir: {d:?}"); + app_builder.additional(&d)?; + } + if let Some(d) = at_most_one(manifest_dir, out_dir.as_deref(), "lib")? { + debug!("Found lib dir: {d:?}"); + app_builder.lib(&d)?; + } + if let Some(d) = at_most_one(manifest_dir, out_dir.as_deref(), "html")? { + debug!("Found html dir: {d:?}"); + app_builder.html(&d)?; + } + + app_builder.build() +} + +fn exactly_one( + manifest_dir: &Path, + out_dir: Option<&Path>, + file_name: &str, +) -> anyhow::Result { + let manifest_file = manifest_dir.join(file_name); + let out_file = out_dir.map(|d| d.join(file_name)); + match ( + manifest_file.exists(), + out_file.as_ref().map(|f| f.exists()).unwrap_or(false), + ) { + (false, false) => bail!("{file_name:?} exists neither {manifest_dir:?} nor {out_dir:?}"), + (false, true) => Ok(out_file.expect("checked above")), + (true, false) => Ok(manifest_file), + (true, true) => bail!("{file_name:?} exist in both {manifest_dir:?} and {out_dir:?}"), + } +} + +fn at_most_one( + manifest_dir: &Path, + out_dir: Option<&Path>, + file_name: &str, +) -> anyhow::Result> { + let manifest_file = manifest_dir.join(file_name); + let out_file = out_dir.map(|d| d.join(file_name)); + match ( + manifest_file.exists(), + out_file.as_ref().map(|f| f.exists()).unwrap_or(false), + ) { + (false, false) => Ok(None), + (false, true) => Ok(Some(out_file.expect("checked above"))), + (true, false) => Ok(Some(manifest_file)), + (true, true) => bail!("{file_name:?} exist in both {manifest_dir:?} and {out_dir:?}"), + } +} + +fn is_app(manifest_path: &Path, out_dir: Option<&Path>) -> bool { + let manifest_dir = manifest_path.parent(); + if let Some(manifest_dir) = manifest_dir { + if manifest_dir.join("manifest.json").is_file() { + debug!("acap manifest found in {manifest_dir:?}"); + return true; + } + } + + if let Some(out_dir) = out_dir { + if out_dir.join("manifest.json").is_file() { + debug!("acap manifest found in {out_dir:?}"); + return true; + } + } + + debug!("acap manifest found neither {manifest_dir:?} nor {out_dir:?}"); + false +} diff --git a/crates/cargo-acap-build/src/command_utils.rs b/crates/cargo-acap-build/src/command_utils.rs new file mode 100644 index 0000000..78b0c52 --- /dev/null +++ b/crates/cargo-acap-build/src/command_utils.rs @@ -0,0 +1,77 @@ +use std::io::{BufRead, BufReader, Read}; + +use anyhow::Context; +use log::debug; + +pub trait RunWith { + fn run_with_captured_stdout(self) -> anyhow::Result; + 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_captured_stdout(mut self) -> anyhow::Result { + self.stdout(std::process::Stdio::piped()); + debug!("Spawning child {self:#?}..."); + let mut child = spawn(self)?; + let mut stdout = child.stdout.take().unwrap(); + let mut decoded = String::new(); + stdout.read_to_string(&mut decoded)?; + debug!("Waiting for child..."); + let status = child.wait()?; + if !status.success() { + anyhow::bail!("Child failed: {status}"); + } + Ok(decoded) + } + + 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/cargo-acap-build/src/lib.rs b/crates/cargo-acap-build/src/lib.rs new file mode 100644 index 0000000..0a02dc3 --- /dev/null +++ b/crates/cargo-acap-build/src/lib.rs @@ -0,0 +1,18 @@ +#![doc=include_str!("../README.md")] +use std::path::PathBuf; + +pub use acap::Architecture; + +mod acap; +mod cargo; +mod cargo_acap; +mod command_utils; + +pub use cargo::get_cargo_metadata; +pub fn build(targets: &[Architecture], args: &[&str]) -> anyhow::Result> { + let mut artifacts = Vec::new(); + for target in targets { + artifacts.extend(cargo_acap::build_and_pack(*target, args)?); + } + Ok(artifacts) +} diff --git a/crates/cargo-acap-build/src/main.rs b/crates/cargo-acap-build/src/main.rs new file mode 100644 index 0000000..ee53aa7 --- /dev/null +++ b/crates/cargo-acap-build/src/main.rs @@ -0,0 +1,138 @@ +use anyhow::{bail, Context}; +use std::fs; +use std::fs::File; +use std::path::PathBuf; + +use cargo_acap_build::{build, get_cargo_metadata, Architecture}; +use clap::{Parser, ValueEnum}; +use log::debug; + +// TODO: Figure out what to call this. +// This is sometimes called just "architecture" but in other contexts arch refers to the first +// part: https://clang.llvm.org/docs/CrossCompilation.html#target-triple +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum)] +enum ArchAbi { + Aarch64, + Armv7hf, +} + +impl From for Architecture { + fn from(val: ArchAbi) -> Self { + match val { + ArchAbi::Aarch64 => Architecture::Aarch64, + ArchAbi::Armv7hf => Architecture::Armv7hf, + } + } +} + +/// ACAP analog to `cargo build`. +#[derive(Parser)] +#[command(version, about, long_about = None)] +#[command(propagate_version = true)] +struct Cli { + /// If given, build only for the given architecture(s). + /// + /// Can be used multiple times. + #[arg(long)] + target: Vec, + /// Pass additional arguments to `cargo build`. + /// + /// Beware that not all incompatible arguments have been documented. + args: Vec, +} + +impl Cli { + pub fn targets(&self) -> Vec { + if self.target.is_empty() { + vec![Architecture::Aarch64, Architecture::Armv7hf] + } else { + self.target.iter().map(|&t| t.into()).collect() + } + } +} + +fn copy_eaps(artifacts: Vec) -> anyhow::Result<()> { + let cargo_target_dir = get_cargo_metadata()?.target_directory; + let acap_dir = cargo_target_dir.join("acap"); + match fs::create_dir(&acap_dir) { + Ok(()) => {} + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(e) => Err(e)?, + } + // Note that: + // - The app name must contain no hyphens and by convention we give the package the same name. + // - Test binaries are usually named like `{package_name}`-{hex_string}` + // This means we should be able to guess the app name from the package name and use the reverse + // of that to give `.eap` files unique names. + // TODO: Consider exposing this from lib instead of guessing it here + for src in artifacts { + if let Some(extension) = src.extension() { + if extension.to_string_lossy() != "eap" { + debug!("{src:?} is not an `.eap`"); + continue; + } + } + let to = src + .parent() + .context(".eap file has no parent dir")? + .file_name() + .context("dir has no name")? + .to_str() + .context("dir name is not a valid string")?; + let parts: Vec<_> = to.split('-').collect(); + let from = match parts.len() { + 0 => panic!("Every string splits into at least one substring"), + 1 | 2 => parts[0], + _ => bail!("Expected dir name with at most one '-' but got {to:?}"), + }; + let name = src + .file_name() + .context("eap has no file name")? + .to_str() + .context("eap file name is not a valid string")? + .replace(from, to); + let dst = acap_dir.join(name); + debug!("Copying `.eap` from {src:?} to {dst:?}"); + fs::copy(src, dst)?; + } + Ok(()) +} + +fn build_and_copy(cli: Cli) -> anyhow::Result<()> { + let targets = cli.targets(); + let args: Vec<_> = cli.args.iter().map(|s| s.as_str()).collect(); + let artifacts = build(&targets, &args)?; + copy_eaps(artifacts) +} + +fn main() -> anyhow::Result<()> { + let log_file = if std::env::var_os("RUST_LOG").is_none() { + if let Some(runtime_dir) = dirs::runtime_dir() { + let path = runtime_dir.join("cargo-acap-build.log"); + let target = env_logger::Target::Pipe(Box::new(File::create(&path)?)); + let mut builder = env_logger::Builder::from_env(env_logger::Env::default()); + builder.target(target).filter_level(log::LevelFilter::Debug); + builder.init(); + Some(path) + } else { + None + } + } else { + env_logger::init(); + None + }; + debug!("Logging initialized"); + + let cli = Cli::parse(); + + match build_and_copy(cli) { + Ok(()) => Ok(()), + Err(e) => { + if let Some(log_file) = log_file { + Err(e.context(format!("A detailed log has been saved to {log_file:?}"))) + } else { + Err(e) + } + } + } +}