Repository: sonos/dinghy Branch: main Commit: b898dfb241ea Files: 77 Total size: 305.0 KB Directory structure: gitextract_xzzdib82/ ├── .github/ │ └── workflows/ │ ├── binaries.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .travis.apple-third-tier.sh ├── .travis.sh ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── cargo-dinghy/ │ ├── Cargo.toml │ └── src/ │ ├── cli.rs │ └── main.rs ├── dinghy-build/ │ ├── Cargo.toml │ └── src/ │ ├── bindgen_macros.rs │ ├── build.rs │ ├── build_env.rs │ ├── lib.rs │ └── utils.rs ├── dinghy-lib/ │ ├── Cargo.toml │ ├── build.rs │ └── src/ │ ├── android/ │ │ ├── device.rs │ │ ├── mod.rs │ │ └── platform.rs │ ├── apple/ │ │ ├── device.rs │ │ ├── helpers.py │ │ ├── mod.rs │ │ ├── platform.rs │ │ └── xcode.rs │ ├── config.rs │ ├── device.rs │ ├── host/ │ │ ├── mod.rs │ │ └── platform.rs │ ├── lib.rs │ ├── overlay.rs │ ├── platform/ │ │ ├── mod.rs │ │ └── regular_platform.rs │ ├── plugin/ │ │ └── mod.rs │ ├── project.rs │ ├── script/ │ │ ├── device.rs │ │ └── mod.rs │ ├── ssh/ │ │ ├── device.rs │ │ └── mod.rs │ ├── toolchain.rs │ └── utils.rs ├── dinghy-test/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── docs/ │ ├── android.md │ ├── dinghy-build.md │ ├── files.md │ ├── filter.md │ ├── ios.md │ ├── overlay.md │ ├── ssh.md │ └── vars.md ├── legacy/ │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src/ │ └── main.rs ├── musl_build.sh ├── post-release.sh ├── pre-release.sh ├── release.sh ├── rust-toolchain └── test-ws/ ├── .dinghy.toml ├── Cargo.toml ├── rust-toolchain ├── test-app/ │ ├── .dinghy.toml │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── test-bin/ │ ├── Cargo.toml │ └── src/ │ └── main.rs └── test-proc-macro/ ├── Cargo.toml └── src/ └── lib.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/binaries.yml ================================================ on: release: types: - created name: Upload Release Assets jobs: assets: name: Upload Release Assets strategy: matrix: os: [ ubuntu-latest, macOS-latest ] include: - os: ubuntu-latest name: linux - os: macOS-latest name: macos runs-on: ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v2 - name: Extract version tag id: version run: echo ::set-output name=value::$(echo ${{ github.ref }} | cut -f 3 -d / ) - name: Build cargo-dinghy run: | set -ex ZIPDIR=cargo-dinghy-${{ steps.version.outputs.value }} mkdir $ZIPDIR if [ ${{matrix.name}} == "macos" ] then # Let's ensure that both aarch64 and x86_64 targets are installed rustup target add aarch64-apple-darwin x86_64-apple-darwin brew install coreutils gnu-tar PATH=$(brew --prefix)/opt/coreutils/libexec/gnubin:$PATH PATH=$(brew --prefix)/opt/gnu-tar/libexec/gnubin:$PATH tar --version cargo build --target aarch64-apple-darwin --target x86_64-apple-darwin --release -p cargo-dinghy lipo -create -output $ZIPDIR/cargo-dinghy target/aarch64-apple-darwin/release/cargo-dinghy target/x86_64-apple-darwin/release/cargo-dinghy else ./musl_build.sh mv target/cargo-dinghy $ZIPDIR fi ls -al $ZIPDIR $ZIPDIR/cargo-dinghy --version file $ZIPDIR/cargo-dinghy md5sum $ZIPDIR/cargo-dinghy tar vczf cargo-dinghy-${{matrix.name}}-${{ steps.version.outputs.value }}.tgz $ZIPDIR md5sum cargo-dinghy-${{matrix.name}}-${{ steps.version.outputs.value }}.tgz mkdir test cd test tar zxf ../cargo-dinghy-${{matrix.name}}-${{ steps.version.outputs.value }}.tgz md5sum $ZIPDIR/cargo-dinghy - name: Upload asset uses: softprops/action-gh-release@v1 with: files: cargo-dinghy-${{matrix.name}}-${{ steps.version.outputs.value }}.tgz name: ${{ steps.version.outputs.value }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yml ================================================ on: push: tags: - 'cargo-dinghy/*' name: Create release jobs: release: name: Create release runs-on: ubuntu-latest steps: - name: Extract version tag id: version run: echo ::set-output name=value::$(echo ${{ github.ref }} | cut -f 4 -d /) - name: Create Release id: create_release uses: actions/create-release@latest env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN_RELEASE }} with: tag_name: ${{ steps.version.outputs.value }} release_name: ${{ steps.version.outputs.value }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Build and test on: pull_request: schedule: - cron: '0 5 * * *' jobs: linux: strategy: fail-fast: false matrix: rust: [ 1.85.0, beta, nightly ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: build and test env: RUST_VERSION: ${{matrix.rust}} run: ./.travis.sh macos: strategy: fail-fast: false matrix: os: [ macOS-14, macOS-15 ] runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v1 - name: build and test run: ./.travis.sh # windows: # runs-on: windows-2019 # # steps: # - uses: actions/checkout@v1 # - name: cargo test # run: cargo test --all apple-3rd-tier: strategy: fail-fast: false matrix: os: [ macOS-14, macOS-15 ] sim: [ tvOS, watchOS ] runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v1 - name: build and test run: ./.travis.apple-third-tier.sh ${{matrix.sim}} env: RUST_VERSION: 1.85.0 linux-musl: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: build and test run: ./musl_build.sh ================================================ FILE: .gitignore ================================================ target .idea/ *.iml *.back ================================================ FILE: .travis.apple-third-tier.sh ================================================ #!/bin/bash set -e set -x SIM_TARGET=$1 title() { set +x echo -e '\n\033[1;33m '$@' \033[0m\n' set -x } if [ -z "$CARGO_DINGHY" ] then title "••• build cargo-dinghy •••" cargo build -p cargo-dinghy CARGO_DINGHY="`pwd`/target/debug/cargo-dinghy -vv" fi echo RUST_VERSION: ${RUST_VERSION:=1.70.0} rustup toolchain add $RUST_VERSION export RUSTUP_TOOLCHAIN=$RUST_VERSION rustup toolchain add nightly --component rust-src; tests_sequence_unstable_target() { # There's something odd with using the .cargo/config runner attribute and # workspaces when the runner uses `cargo run --manifest-path ../Cargo.toml # --bin cargo-dinghy ...` title "testing from project directory for rust target $1 on device $2" title "testing from workspace directory" ( \ cd test-ws \ && cargo clean \ && $CARGO_DINGHY -d $1 -p $2 +nightly test -Zbuild-std pass \ && ! $CARGO_DINGHY -d $1 -p $2 +nightly test -Zbuild-std fails \ && ! $CARGO_DINGHY -d $1 -p $2 +nightly test -Zbuild-std \ ) title "testing from project directory" ( \ cd test-ws/test-app \ && cargo clean \ && $CARGO_DINGHY -d $1 -p $2 +nightly test -Zbuild-std pass \ && ! $CARGO_DINGHY -d $1 -p $2 +nightly test -Zbuild-std fails \ && ! $CARGO_DINGHY -d $1 -p $2 +nightly test -Zbuild-std \ ) title "test from workspace directory with project filter" ( \ cd test-ws \ && cargo clean \ && $CARGO_DINGHY -d $1 -p $2 +nightly test -p test-app -Zbuild-std pass \ && ! $CARGO_DINGHY -d $1 -p $2 +nightly test -p test-app -Zbuild-std fails \ && ! $CARGO_DINGHY -d $1 -p $2 +nightly test -p test-app -Zbuild-std \ ) } if [ $SIM_TARGET = tvOS ] then title "••••• Darwin: tvos simulator tests •••••" title "boot a simulator" # *-apple-{tvos,watchos}[-sim] require `-Zbuild-std` TVOS_RUNTIME_ID=$(xcrun simctl list runtimes | grep tvOS | cut -d ' ' -f 7 | tail -1) export TV_SIM_ID=$(xcrun simctl create My-4ktv com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-4K $TVOS_RUNTIME_ID) xcrun simctl boot $TV_SIM_ID if [ "$(uname -m)" = "arm64" ]; then tests_sequence_unstable_target ${TV_SIM_ID} auto-tvos-aarch64-sim else # The x86 tvOS simulator tripple does not end in -sim. tests_sequence_unstable_target ${TV_SIM_ID} auto-tvos-x86_64 fi xcrun simctl delete $TV_SIM_ID fi if [ $SIM_TARGET = watchOS ] then title "••••• Darwin: watchvos simulator tests •••••" title "boot a simulator" WATCHOS_RUNTIME_ID=$(xcrun simctl list runtimes | grep watchOS | cut -d ' ' -f 7 | tail -1) export WATCHOS_SIM_ID=$(xcrun simctl create My-apple-watch com.apple.CoreSimulator.SimDeviceType.Apple-Watch-SE-44mm-2nd-generation $WATCHOS_RUNTIME_ID) xcrun simctl boot $WATCHOS_SIM_ID if [ "$(uname -m)" = "arm64" ]; then tests_sequence_unstable_target ${WATCHOS_SIM_ID} auto-watchos-aarch64-sim else tests_sequence_unstable_target ${WATCHOS_SIM_ID} auto-watchos-x86_64-sim fi xcrun simctl delete $WATCHOS_SIM_ID fi # This depends on https://github.com/sonos/dinghy/pull/223 if [ $SIM_TARGET = visionOS ] then if [ "$(uname -m)" = "arm64" ]; then title "••••• Darwin: visionOS simulator tests •••••" title "boot a simulator" xcrun simctl list devicetypes vision VISIONOS_DEVICE_TYPE=$(xcrun simctl list devicetypes vision -j | jq -r '.devicetypes[0].identifier') VISIONOS_RUNTIME_ID=$(xcrun simctl list runtimes | grep visionOS | cut -d ' ' -f 7 | tail -1) export VISIONOS_SIM_ID=$(xcrun simctl create My-apple-vision-pro $VISIONOS_DEVICE_TYPE $VISIONOS_RUNTIME_ID) xcrun simctl boot $VISIONOS_SIM_ID tests_sequence_unstable_target ${VISIONOS_SIM_ID} auto-visionos-aarch64-sim xcrun simctl delete $VISIONOS_SIM_ID fi fi rustup default stable ================================================ FILE: .travis.sh ================================================ #!/bin/bash set -e set -x title() { set +x echo -e '\n\033[1;33m '$@' \033[0m\n' set -x } if [ ! cargo --version ] then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y PATH=$PATH:$HOME/.cargo/bin fi if [ -z "$CARGO_DINGHY" ] then title "••• build cargo-dinghy •••" cargo build -p cargo-dinghy CARGO_DINGHY="`pwd`/target/debug/cargo-dinghy -vv" fi echo RUST_VERSION: ${RUST_VERSION:=1.85.0} rustup toolchain add $RUST_VERSION export RUSTUP_TOOLCHAIN=$RUST_VERSION title "••• test original cargo build •••" cargo build cargo test ( \ cd test-ws/test-app \ && export NOT_BUILT_WITH_DINGHY=1 \ && cargo test pass \ && ! NOT_BUILT_WITH_DINGHY=1 cargo test fails \ ) tests_sequence() { title "testing from workspace directory" ( \ cd test-ws \ && cargo clean \ && $CARGO_DINGHY -d $1 test pass \ && $CARGO_DINGHY -d $1 test --doc \ && ! $CARGO_DINGHY -d $1 test fails \ && ! $CARGO_DINGHY -d $1 test \ ) title "testing from project directory" ( \ cd test-ws/test-app \ && cargo clean \ && $CARGO_DINGHY -d $1 test pass \ && $CARGO_DINGHY -d $1 test --doc \ && ! $CARGO_DINGHY -d $1 test fails \ && ! $CARGO_DINGHY -d $1 test \ ) title "test from workspace directory with project filter" ( \ cd test-ws \ && cargo clean \ && $CARGO_DINGHY -d $1 test -p test-app pass \ && $CARGO_DINGHY -d $1 test -p test-app --doc \ && ! $CARGO_DINGHY -d $1 test -p test-app fails \ && ! $CARGO_DINGHY -d $1 test -p test-app \ ) } tests_sequence_aarch64_ios_sim() { title "testing from workspace directory" ( \ cd test-ws \ && cargo clean \ && $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test pass \ && $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test --doc \ && ! $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test fails \ && ! $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test \ ) title "testing from project directory" ( \ cd test-ws/test-app \ && cargo clean \ && $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test pass \ && $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test --doc \ && ! $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test fails \ && ! $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test \ ) title "test from workspace directory with project filter" ( \ cd test-ws \ && cargo clean \ && $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test -p test-app pass \ && $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test -p test-app --doc \ && ! $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test -p test-app fails \ && ! $CARGO_DINGHY -d $1 -p auto-ios-aarch64-sim test -p test-app \ ) } if [ `uname` = Darwin ] then title "••••• Darwin: ios simulator tests •••••" title "boot a simulator" RUNTIME_ID=$(xcrun simctl list runtimes | grep iOS | cut -d ' ' -f 7 | tail -1) # Installed simulators on github runners differ depending on the version # of macos. When the simulator device type ID needs to be updated, select # a new one: # https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md#installed-simulators # https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md#installed-simulators # https://github.com/actions/runner-images/blob/main/images/macos/macos-14-arm64-Readme.md#installed-simulators export SIM_ID=$(xcrun simctl create My-iphone-se com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation $RUNTIME_ID) xcrun simctl boot $SIM_ID # The x86_64-apple-ios target seems to not work on an ARM64 host, # and the aarch64-apple-ios-sim target doesn't work on an x86-64 host. if [ "$(uname -m)" = "arm64" ]; then rustup target add aarch64-apple-ios-sim; tests_sequence_aarch64_ios_sim $SIM_ID else rustup target add x86_64-apple-ios; tests_sequence $SIM_ID fi xcrun simctl delete $SIM_ID if ios-deploy -c -t 1 > /tmp/ios_devices then device=$(grep "Found" /tmp/ios_devices | head -1 | cut -d " " -f 3) title "••••• Darwin: ios-deploy detected a device •••••" rustup target add aarch64-apple-ios tests_sequence $device fi else echo $ANDROID_SDK_ROOT if [ -n "$ANDROID_SDK_ROOT" ] then title "••••• Linux: android tests •••••" title "setup simulator" rustup target add armv7-linux-androideabi ## BEGIN FIX-EMULATOR # Use emulator version 32.1.15 as latest version (36.3.10.0 as of writing) # doesn't support arm images anymore EMULATOR="$(pwd)/target/emulator/emulator" [ -e $EMULATOR ] || ( \ cd target/ \ && wget -q https://redirector.gvt1.com/edgedl/android/repository/emulator-linux_x64-10696886.zip \ && unzip emulator-linux_x64-10696886.zip \ && rm emulator-linux_x64-10696886.zip ) # to use the bundled emulator instead (if we choose to go the x86 route at some point) # use the using the following line instead # EMULATOR="$ANDROID_SDK_ROOT/emulator/emulator" # END FIX-EMULATOR yes | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "system-images;android-24;default;armeabi-v7a" "ndk;27.3.13750724" "emulator" "platform-tools" # "cmdline-tools;latest" echo no | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager create avd -c 1000M --force -n testdinghy -k "system-images;android-24;default;armeabi-v7a" ANDROID_AVD_HOME=$HOME/.config/.android/avd $EMULATOR @testdinghy -partition-size 1024 -no-audio -no-boot-anim -no-window -accel on -gpu off & timeout 180 $ANDROID_SDK_ROOT/platform-tools/adb wait-for-device export ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk/27.3.13750724 tests_sequence android fi title "••••• Linux: script tests (with qemu) •••••" title "setup qemu" rustup target add aarch64-unknown-linux-musl sudo apt-get update sudo apt-get -y install --no-install-recommends qemu-system-arm qemu-user binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu echo -e "[platforms.qemu]\nrustc_triple='aarch64-unknown-linux-musl'\ndeb_multiarch='aarch64-linux-gnu'" > .dinghy.toml echo -e "[script_devices.qemu]\nplatform='qemu'\npath='/tmp/qemu'" >> .dinghy.toml echo -e "#!/bin/sh\nexe=\$1\nshift\n/usr/bin/qemu-aarch64 -L /usr/aarch64-linux-gnu/ \$exe --test-threads 1 \"\$@\"" > /tmp/qemu chmod +x /tmp/qemu tests_sequence qemu fi if [ -n "$DEPLOY" ] then if [ `uname` = Linux ] then export OPENSSL_STATIC=yes export OPENSSL_INCLUDE_DIR=/usr/include export OPENSSL_LIB_DIR=$(dirname `find /usr -name libssl.a`) cargo clean fi cargo build --release -p cargo-dinghy mkdir -p cargo-dinghy-$DEPLOY cp target/release/cargo-dinghy cargo-dinghy-$DEPLOY tar vczf cargo-dinghy-$DEPLOY.tgz cargo-dinghy-$DEPLOY fi ================================================ FILE: .travis.yml ================================================ sudo: false language: rust matrix: allow_failures: - rust: nightly include: - os: osx osx_image: xcode11.2 env: DEPLOY=macos - os: linux dist: bionic - rust: stable dist: bionic env: DEPLOY=travis - rust: beta dist: bionic - rust: nightly dist: bionic script: sh .travis.sh deploy: provider: releases skip_cleanup: true api_key: secure: YJlTT/zACnSNi3jFVXF+pPx615hxUrRe5ZCLLd3bgek/daEI4FIYJQIbO8ZlL8ufx7/SVKg8gjGxNYqDUsd3dozjVlALibogi6XBZfoAWZBkCUQNyoqALzNnTP+PbKme5mRQbtNPp5X0g7KDYoJGUHsvD6FUD7rxYe6QMpbGUvaa89cwFguNvWsjZgGBYtf5HO42u+JE6hXhmO8WGyK0cQYZEdXWn9kUBgQD1ua4+rqn9h35IFveEG0QAq59HNDTfmg67okXJINqYlJ0DzGJM4yN8BH9nSsBD/vGn79Z7nLp6PXOBIvwBY2sRUt3WwVNLGAVCqlmIeRdioQbUvJm09uqgNt8O9vKZYcAxUYja1xfCO5lyUDQ8IYJ5EZU9lSpW3wvalLKswL6bvLQ1ckI2ASe8rYMxsbUZ5TE3fJWuHpuGkaoJnvIvefrR4orB/Gi5cSJ708TBtUwS/Law95hUHPbCKs6nsC1vsoLLj33xZqpoNCZH8BSWyDWsfSuDdiMrSW68LeYe/DxWFuSg/AWfqDXi5xY5CMFzRJ5TgxhZ7wGVvrUTAvripVyPQkewb3fThgLDGqAwboPq9wq7UqiJKT5l8/BOHdlKTEeX5AKlaMCERZtupM6LT/2v3YuaBZpiVnsDW8nWAz0DyEcFcWo/UFl44Njrah9f/t4tJXuF7k= file_glob: true file: cargo-dinghy-*.tgz on: tags: true repo: sonos/dinghy condition: "$TRAVIS_TAG =~ ^.*$" ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [0.8.4](https://github.com/sonos/dinghy/tree/0.8.4) (2025-12-12) [Full Changelog](https://github.com/sonos/dinghy/compare/0.8.2...0.8.4) **Merged pull requests:** - bump macos [\#261](https://github.com/sonos/dinghy/pull/261) ([kali](https://github.com/kali)) - use 1.85 for iDevices [\#259](https://github.com/sonos/dinghy/pull/259) ([kali](https://github.com/kali)) ## [0.8.2](https://github.com/sonos/dinghy/tree/0.8.2) (2025-10-07) [Full Changelog](https://github.com/sonos/dinghy/compare/0.8.1...0.8.2) **Closed issues:** - Doctests freeze on Rust 1.89 [\#255](https://github.com/sonos/dinghy/issues/255) **Merged pull requests:** - 1.85.0 [\#258](https://github.com/sonos/dinghy/pull/258) ([kali](https://github.com/kali)) - ignore devices with no udid [\#257](https://github.com/sonos/dinghy/pull/257) ([kali](https://github.com/kali)) - 1.89 doctests support [\#256](https://github.com/sonos/dinghy/pull/256) ([fredszaq](https://github.com/fredszaq)) - fix ci by holding back some deps [\#254](https://github.com/sonos/dinghy/pull/254) ([kali](https://github.com/kali)) - Fix display of android platforms in cargo dinghy all-devices [\#250](https://github.com/sonos/dinghy/pull/250) ([fredszaq](https://github.com/fredszaq)) ## [0.8.1](https://github.com/sonos/dinghy/tree/0.8.1) (2025-06-30) [Full Changelog](https://github.com/sonos/dinghy/compare/0.8.0...0.8.1) **Closed issues:** - No devices found for name hint `my\_android' [\#252](https://github.com/sonos/dinghy/issues/252) **Merged pull requests:** - Use VersionReq api to support X.Y versions \(needed for iOS 18\) [\#253](https://github.com/sonos/dinghy/pull/253) ([hdlj](https://github.com/hdlj)) - fix workspace resolver warning in test-ws [\#249](https://github.com/sonos/dinghy/pull/249) ([fredszaq](https://github.com/fredszaq)) - search ndk in default install location in linux [\#248](https://github.com/sonos/dinghy/pull/248) ([fredszaq](https://github.com/fredszaq)) - debug ci [\#247](https://github.com/sonos/dinghy/pull/247) ([kali](https://github.com/kali)) - Fix provisioning profile path detection [\#246](https://github.com/sonos/dinghy/pull/246) ([emricksinisonos](https://github.com/emricksinisonos)) - Revert "Disable proc macro tests for tvOS and watchOS" [\#244](https://github.com/sonos/dinghy/pull/244) ([fredszaq](https://github.com/fredszaq)) ## [0.8.0](https://github.com/sonos/dinghy/tree/0.8.0) (2024-11-18) [Full Changelog](https://github.com/sonos/dinghy/compare/0.7.3...0.8.0) **Closed issues:** - WathOS CI broken [\#238](https://github.com/sonos/dinghy/issues/238) **Merged pull requests:** - Disable proc macro tests for tvOS and watchOS [\#243](https://github.com/sonos/dinghy/pull/243) ([fredszaq](https://github.com/fredszaq)) - Introduce plugin platform [\#242](https://github.com/sonos/dinghy/pull/242) ([fredszaq](https://github.com/fredszaq)) - add `skip_source_copy` flag [\#241](https://github.com/sonos/dinghy/pull/241) ([fredszaq](https://github.com/fredszaq)) ## [0.7.3](https://github.com/sonos/dinghy/tree/0.7.3) (2024-10-16) [Full Changelog](https://github.com/sonos/dinghy/compare/0.7.2...0.7.3) **Merged pull requests:** - XCode 16: profiles location has changed [\#239](https://github.com/sonos/dinghy/pull/239) ([kali](https://github.com/kali)) - accept licenses [\#236](https://github.com/sonos/dinghy/pull/236) ([kali](https://github.com/kali)) ## [0.7.2](https://github.com/sonos/dinghy/tree/0.7.2) (2024-06-17) [Full Changelog](https://github.com/sonos/dinghy/compare/0.7.1...0.7.2) **Closed issues:** - About the build performance [\#232](https://github.com/sonos/dinghy/issues/232) - xcrun: error: unable to find utility "devicectl", not a developer tool or in PATH [\#225](https://github.com/sonos/dinghy/issues/225) **Merged pull requests:** - remove deprecated atty and tempdir dependencies [\#235](https://github.com/sonos/dinghy/pull/235) ([fredszaq](https://github.com/fredszaq)) - cargo fmt [\#234](https://github.com/sonos/dinghy/pull/234) ([ThibautLorrainSonos](https://github.com/ThibautLorrainSonos)) - libclang path is in lib and not lib64 in ndk 26+ [\#233](https://github.com/sonos/dinghy/pull/233) ([ThibautLorrainSonos](https://github.com/ThibautLorrainSonos)) - Update github org [\#231](https://github.com/sonos/dinghy/pull/231) ([jayvdb](https://github.com/jayvdb)) - show id of ssh devices \(in addtion to their ip\) in all-devices command [\#230](https://github.com/sonos/dinghy/pull/230) ([fredszaq](https://github.com/fredszaq)) - Use matrix in CI for third tier apple simulators [\#228](https://github.com/sonos/dinghy/pull/228) ([simlay](https://github.com/simlay)) ## [0.7.1](https://github.com/sonos/dinghy/tree/0.7.1) (2024-04-11) [Full Changelog](https://github.com/sonos/dinghy/compare/0.7.0...0.7.1) **Merged pull requests:** - fix macos binary upload [\#227](https://github.com/sonos/dinghy/pull/227) ([kali](https://github.com/kali)) - only warn if xcrun is missing [\#226](https://github.com/sonos/dinghy/pull/226) ([kali](https://github.com/kali)) - Bump msrv to 1.74 [\#224](https://github.com/sonos/dinghy/pull/224) ([simlay](https://github.com/simlay)) ## [0.7.0](https://github.com/sonos/dinghy/tree/0.7.0) (2024-04-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.6.8...0.7.0) **Closed issues:** - Run test on iOS failed: No such file or directory \(os error 2\) [\#216](https://github.com/sonos/dinghy/issues/216) - Support cargo-dinghy as a cargo test runner [\#210](https://github.com/sonos/dinghy/issues/210) - Dinghy hangs on macOS-14 runner [\#207](https://github.com/sonos/dinghy/issues/207) - Use `xcrun devicectl` to deploy to iOS devices. [\#204](https://github.com/sonos/dinghy/issues/204) - Faiiled to build what use ndk\_context project [\#201](https://github.com/sonos/dinghy/issues/201) - Add macOS M1 binary to release CI [\#200](https://github.com/sonos/dinghy/issues/200) - Getting files generated by tests back? [\#199](https://github.com/sonos/dinghy/issues/199) - `undefined symbol: ANativeWindow_setBuffersGeometry` with visual tests using `wgpu` [\#198](https://github.com/sonos/dinghy/issues/198) - watchOS simulator support [\#194](https://github.com/sonos/dinghy/issues/194) - tvOS simulator support [\#193](https://github.com/sonos/dinghy/issues/193) - How to build with non-default crate features? [\#129](https://github.com/sonos/dinghy/issues/129) **Merged pull requests:** - use fs\_err when it make sense in dinghy\_lib [\#222](https://github.com/sonos/dinghy/pull/222) ([kali](https://github.com/kali)) - follow symlinks while walking dirs \(and improve error messages\) [\#221](https://github.com/sonos/dinghy/pull/221) ([kali](https://github.com/kali)) - fix when multiple Dinghy app are installed [\#220](https://github.com/sonos/dinghy/pull/220) ([kali](https://github.com/kali)) - add changelog generation script in ./pre-release.sh [\#219](https://github.com/sonos/dinghy/pull/219) ([fredszaq](https://github.com/fredszaq)) - better error message on pymobiledevice3 failure [\#218](https://github.com/sonos/dinghy/pull/218) ([kali](https://github.com/kali)) - android: fix SDK detection on mac. [\#217](https://github.com/sonos/dinghy/pull/217) ([ashdnazg](https://github.com/ashdnazg)) - Support for ios17 [\#215](https://github.com/sonos/dinghy/pull/215) ([kali](https://github.com/kali)) - Infer the platform when runner is called in standalone mode [\#213](https://github.com/sonos/dinghy/pull/213) ([fredszaq](https://github.com/fredszaq)) - Universal macos binary in CI [\#209](https://github.com/sonos/dinghy/pull/209) ([simlay](https://github.com/simlay)) - Test on macOS-13 \(x86\) and macOS-14 \(arm\) [\#206](https://github.com/sonos/dinghy/pull/206) ([simlay](https://github.com/simlay)) - android emulator needs to be installed for avdmanager to work [\#205](https://github.com/sonos/dinghy/pull/205) ([ThibautLorrainSonos](https://github.com/ThibautLorrainSonos)) - Support tvOS and watchOS simulators [\#203](https://github.com/sonos/dinghy/pull/203) ([simlay](https://github.com/simlay)) - use android emulator 32.1.15 in ci as 33.1.23 segfaults [\#202](https://github.com/sonos/dinghy/pull/202) ([fredszaq](https://github.com/fredszaq)) - Bump rustix from 0.38.14 to 0.38.19 [\#196](https://github.com/sonos/dinghy/pull/196) ([dependabot[bot]](https://github.com/apps/dependabot)) - Fix preferences path after Ventura [\#195](https://github.com/sonos/dinghy/pull/195) ([ldm0](https://github.com/ldm0)) ## [0.6.8](https://github.com/sonos/dinghy/tree/0.6.8) (2023-09-29) [Full Changelog](https://github.com/sonos/dinghy/compare/0.6.7...0.6.8) **Merged pull requests:** - search android ndk also in ANDROID\_NDK env var and pass ANDROID\_NDK var to build if not set \(cmake compat\) [\#192](https://github.com/sonos/dinghy/pull/192) ([fredszaq](https://github.com/fredszaq)) ## [0.6.7](https://github.com/sonos/dinghy/tree/0.6.7) (2023-09-14) [Full Changelog](https://github.com/sonos/dinghy/compare/0.6.6...0.6.7) **Closed issues:** - Maintain CHANGELOG [\#187](https://github.com/sonos/dinghy/issues/187) **Merged pull requests:** - Do not crash dinghy\_bindgen macro if DINGHY\_BUILD\_LIBCLANG\_PATH is not present [\#191](https://github.com/sonos/dinghy/pull/191) ([fredszaq](https://github.com/fredszaq)) ## [0.6.6](https://github.com/sonos/dinghy/tree/0.6.6) (2023-09-14) [Full Changelog](https://github.com/sonos/dinghy/compare/0.6.5...0.6.6) **Merged pull requests:** - msrv 1.70 [\#190](https://github.com/sonos/dinghy/pull/190) ([fredszaq](https://github.com/fredszaq)) - fix bindgen config when using android ndk [\#189](https://github.com/sonos/dinghy/pull/189) ([fredszaq](https://github.com/fredszaq)) ## [0.6.5](https://github.com/sonos/dinghy/tree/0.6.5) (2023-09-11) [Full Changelog](https://github.com/sonos/dinghy/compare/0.6.4...0.6.5) **Closed issues:** - `catch_unwind` broken on `armv7-apple-ios` [\#156](https://github.com/sonos/dinghy/issues/156) **Merged pull requests:** - properly setup clang++ as CXX in android auto platforms [\#188](https://github.com/sonos/dinghy/pull/188) ([fredszaq](https://github.com/fredszaq)) - cargo update \(procmacro2 failing to compile on nightly\) [\#186](https://github.com/sonos/dinghy/pull/186) ([fredszaq](https://github.com/fredszaq)) - make RUST\_BACKTRACE overwritable by --env [\#185](https://github.com/sonos/dinghy/pull/185) ([bestouff](https://github.com/bestouff)) - add legacy dinghy crate [\#184](https://github.com/sonos/dinghy/pull/184) ([fredszaq](https://github.com/fredszaq)) - bump msrv [\#183](https://github.com/sonos/dinghy/pull/183) ([kali](https://github.com/kali)) ## [0.6.4](https://github.com/sonos/dinghy/tree/0.6.4) (2023-04-13) [Full Changelog](https://github.com/sonos/dinghy/compare/0.6.3...0.6.4) **Merged pull requests:** - use llvm-ar instead of the binutils one for android ndk 23+ [\#182](https://github.com/sonos/dinghy/pull/182) ([fredszaq](https://github.com/fredszaq)) ## [0.6.3](https://github.com/sonos/dinghy/tree/0.6.3) (2022-11-18) [Full Changelog](https://github.com/sonos/dinghy/compare/0.6.2...0.6.3) **Closed issues:** - On the use `std::fs::ReadDir.next()` [\#177](https://github.com/sonos/dinghy/issues/177) - failed to compile cargo-dinghy v0.3.2 - multiple packages link to native library git2 [\#66](https://github.com/sonos/dinghy/issues/66) **Merged pull requests:** - add all shared libs as args in run-with [\#181](https://github.com/sonos/dinghy/pull/181) ([fredszaq](https://github.com/fredszaq)) - bump iphone min version [\#180](https://github.com/sonos/dinghy/pull/180) ([kali](https://github.com/kali)) - add apt update [\#179](https://github.com/sonos/dinghy/pull/179) ([kali](https://github.com/kali)) ## [0.6.2](https://github.com/sonos/dinghy/tree/0.6.2) (2022-08-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.6.1...0.6.2) **Closed issues:** - No more working on iOS devices 14.x [\#135](https://github.com/sonos/dinghy/issues/135) - Cannot run tests on iOS 14 device [\#131](https://github.com/sonos/dinghy/issues/131) - test --no-fail-fast is not recognized [\#127](https://github.com/sonos/dinghy/issues/127) - Old android device using ro.product.cpu.abi rather than ro.product.cpu.abilist [\#109](https://github.com/sonos/dinghy/issues/109) - ImportError: No module named six [\#6](https://github.com/sonos/dinghy/issues/6) **Merged pull requests:** - fix android toolchain discovery on macOS [\#178](https://github.com/sonos/dinghy/pull/178) ([fredszaq](https://github.com/fredszaq)) - Mention ios-deploy in docu [\#176](https://github.com/sonos/dinghy/pull/176) ([umgefahren](https://github.com/umgefahren)) ## [0.6.1](https://github.com/sonos/dinghy/tree/0.6.1) (2022-08-04) [Full Changelog](https://github.com/sonos/dinghy/compare/0.6.0...0.6.1) **Closed issues:** - deploy to iOS devices with ios-deploy [\#166](https://github.com/sonos/dinghy/issues/166) - No device support directory for iOS version 12.5 [\#165](https://github.com/sonos/dinghy/issues/165) - lldb output is variable [\#158](https://github.com/sonos/dinghy/issues/158) **Merged pull requests:** - musl build fix [\#175](https://github.com/sonos/dinghy/pull/175) ([fredszaq](https://github.com/fredszaq)) - display compile errors and check cargo exit status in run-with [\#174](https://github.com/sonos/dinghy/pull/174) ([fredszaq](https://github.com/fredszaq)) - Dependency cleanup [\#172](https://github.com/sonos/dinghy/pull/172) ([madsmtm](https://github.com/madsmtm)) - Support newer versions of the OpenSSL x509 text format [\#171](https://github.com/sonos/dinghy/pull/171) ([madsmtm](https://github.com/madsmtm)) ## [0.6.0](https://github.com/sonos/dinghy/tree/0.6.0) (2022-07-27) [Full Changelog](https://github.com/sonos/dinghy/compare/0.5.1...0.6.0) **Closed issues:** - Share resource files between tests [\#161](https://github.com/sonos/dinghy/issues/161) **Merged pull requests:** - some ios signing logging adjusts [\#170](https://github.com/sonos/dinghy/pull/170) ([kali](https://github.com/kali)) - Logging overhaul [\#169](https://github.com/sonos/dinghy/pull/169) ([fredszaq](https://github.com/fredszaq)) - use ios-deploy, remove in-house rust partial port [\#168](https://github.com/sonos/dinghy/pull/168) ([kali](https://github.com/kali)) - introduce run-with subcommand and transparent copy of files in runner args [\#167](https://github.com/sonos/dinghy/pull/167) ([fredszaq](https://github.com/fredszaq)) - do not copy ad-hoc rsync on device if file exists [\#164](https://github.com/sonos/dinghy/pull/164) ([fredszaq](https://github.com/fredszaq)) - Use package name instead of runnable id for dir on target [\#163](https://github.com/sonos/dinghy/pull/163) ([fredszaq](https://github.com/fredszaq)) - support android ndk 23 and up [\#162](https://github.com/sonos/dinghy/pull/162) ([fredszaq](https://github.com/fredszaq)) - Broken implicit wp dep [\#160](https://github.com/sonos/dinghy/pull/160) ([kali](https://github.com/kali)) ## [0.5.1](https://github.com/sonos/dinghy/tree/0.5.1) (2022-07-08) [Full Changelog](https://github.com/sonos/dinghy/compare/0.5.0...0.5.1) **Merged pull requests:** - try to make the excluded bug appear in CI [\#159](https://github.com/sonos/dinghy/pull/159) ([kali](https://github.com/kali)) ## [0.5.0](https://github.com/sonos/dinghy/tree/0.5.0) (2022-07-06) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.71...0.5.0) **Closed issues:** - cargo install dinghy takes too long [\#39](https://github.com/sonos/dinghy/issues/39) **Merged pull requests:** - Remove bundled cargo [\#157](https://github.com/sonos/dinghy/pull/157) ([fredszaq](https://github.com/fredszaq)) - Update cargo to v0.62 [\#154](https://github.com/sonos/dinghy/pull/154) ([madsmtm](https://github.com/madsmtm)) ## [0.4.71](https://github.com/sonos/dinghy/tree/0.4.71) (2022-03-21) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.70...0.4.71) **Closed issues:** - Support for aarch64-apple-ios-sim in dinghy [\#147](https://github.com/sonos/dinghy/issues/147) **Merged pull requests:** - Fix iOS bundle to make app use the full screen [\#152](https://github.com/sonos/dinghy/pull/152) ([simlay](https://github.com/simlay)) - Initial stuff for aarch64 ios simulator support [\#151](https://github.com/sonos/dinghy/pull/151) ([simlay](https://github.com/simlay)) ## [0.4.70](https://github.com/sonos/dinghy/tree/0.4.70) (2022-03-18) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.69...0.4.70) **Merged pull requests:** - bump cargo [\#150](https://github.com/sonos/dinghy/pull/150) ([kali](https://github.com/kali)) ## [0.4.69](https://github.com/sonos/dinghy/tree/0.4.69) (2022-03-17) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.68...0.4.69) **Closed issues:** - Rust 2021 Edition [\#148](https://github.com/sonos/dinghy/issues/148) **Merged pull requests:** - Fix iOS simulator tests with lldb 13 [\#149](https://github.com/sonos/dinghy/pull/149) ([simlay](https://github.com/simlay)) - tests are actually runnable on host platform [\#146](https://github.com/sonos/dinghy/pull/146) ([kali](https://github.com/kali)) - Fix ci weirdness [\#145](https://github.com/sonos/dinghy/pull/145) ([kali](https://github.com/kali)) - Fix iOS 9.3 [\#143](https://github.com/sonos/dinghy/pull/143) ([madsmtm](https://github.com/madsmtm)) - Fix "Developer" typo [\#142](https://github.com/sonos/dinghy/pull/142) ([madsmtm](https://github.com/madsmtm)) ## [0.4.68](https://github.com/sonos/dinghy/tree/0.4.68) (2022-01-19) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.67...0.4.68) **Merged pull requests:** - Dont run tests for `proc-macro` crates [\#144](https://github.com/sonos/dinghy/pull/144) ([madsmtm](https://github.com/madsmtm)) ## [0.4.67](https://github.com/sonos/dinghy/tree/0.4.67) (2021-12-06) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.66...0.4.67) **Merged pull requests:** - add support for custom profiles [\#140](https://github.com/sonos/dinghy/pull/140) ([fredszaq](https://github.com/fredszaq)) ## [0.4.66](https://github.com/sonos/dinghy/tree/0.4.66) (2021-11-29) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.63...0.4.66) **Closed issues:** - test-app build fails with unable to find library -lgcc [\#138](https://github.com/sonos/dinghy/issues/138) **Merged pull requests:** - manual import from anyhow [\#139](https://github.com/sonos/dinghy/pull/139) ([kali](https://github.com/kali)) ## [0.4.63](https://github.com/sonos/dinghy/tree/0.4.63) (2021-11-04) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.62...0.4.63) **Closed issues:** - Support building crates that use resolver 2 [\#133](https://github.com/sonos/dinghy/issues/133) **Merged pull requests:** - update cargo and other dependencies [\#137](https://github.com/sonos/dinghy/pull/137) ([fredszaq](https://github.com/fredszaq)) ## [0.4.62](https://github.com/sonos/dinghy/tree/0.4.62) (2021-07-28) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.61...0.4.62) **Merged pull requests:** - Bump cargo [\#134](https://github.com/sonos/dinghy/pull/134) ([kali](https://github.com/kali)) - Fixes the iOS test command [\#132](https://github.com/sonos/dinghy/pull/132) ([Robert-Steiner](https://github.com/Robert-Steiner)) ## [0.4.61](https://github.com/sonos/dinghy/tree/0.4.61) (2021-02-15) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.60...0.4.61) ## [0.4.60](https://github.com/sonos/dinghy/tree/0.4.60) (2021-02-15) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.59...0.4.60) ## [0.4.59](https://github.com/sonos/dinghy/tree/0.4.59) (2021-02-15) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.57...0.4.59) ## [0.4.57](https://github.com/sonos/dinghy/tree/0.4.57) (2021-02-15) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.55...0.4.57) ## [0.4.55](https://github.com/sonos/dinghy/tree/0.4.55) (2021-02-15) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.54...0.4.55) ## [0.4.54](https://github.com/sonos/dinghy/tree/0.4.54) (2021-02-15) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.53...0.4.54) ## [0.4.53](https://github.com/sonos/dinghy/tree/0.4.53) (2021-02-15) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.52...0.4.53) ## [0.4.52](https://github.com/sonos/dinghy/tree/0.4.52) (2021-02-15) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.51...0.4.52) ## [0.4.51](https://github.com/sonos/dinghy/tree/0.4.51) (2021-02-15) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.50...0.4.51) ## [0.4.50](https://github.com/sonos/dinghy/tree/0.4.50) (2021-02-12) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.48...0.4.50) ## [0.4.48](https://github.com/sonos/dinghy/tree/0.4.48) (2021-02-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.47...0.4.48) ## [0.4.47](https://github.com/sonos/dinghy/tree/0.4.47) (2021-02-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.46...0.4.47) ## [0.4.46](https://github.com/sonos/dinghy/tree/0.4.46) (2021-02-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.45...0.4.46) ## [0.4.45](https://github.com/sonos/dinghy/tree/0.4.45) (2021-02-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.43...0.4.45) ## [0.4.43](https://github.com/sonos/dinghy/tree/0.4.43) (2021-02-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.41...0.4.43) **Closed issues:** - don't work for ios 13.3 on osx 10.15.5 [\#116](https://github.com/sonos/dinghy/issues/116) **Merged pull requests:** - Maintenance [\#130](https://github.com/sonos/dinghy/pull/130) ([kali](https://github.com/kali)) - Better error message on rsync exec [\#128](https://github.com/sonos/dinghy/pull/128) ([fredszaq](https://github.com/fredszaq)) - Fix some grammar and typos in README.md [\#126](https://github.com/sonos/dinghy/pull/126) ([geophree](https://github.com/geophree)) ## [0.4.41](https://github.com/sonos/dinghy/tree/0.4.41) (2020-10-16) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.40...0.4.41) **Closed issues:** - Copies libdl.so to Android, causing failure to run [\#124](https://github.com/sonos/dinghy/issues/124) - Dinghy on Linux [\#121](https://github.com/sonos/dinghy/issues/121) **Merged pull requests:** - Prevent deployment of libdl.so on Android [\#125](https://github.com/sonos/dinghy/pull/125) ([tom-bowles](https://github.com/tom-bowles)) ## [0.4.40](https://github.com/sonos/dinghy/tree/0.4.40) (2020-09-10) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.39...0.4.40) **Merged pull requests:** - Check Android NDK in non legacy path [\#123](https://github.com/sonos/dinghy/pull/123) ([kafji](https://github.com/kafji)) - Log on Android NDK not found [\#122](https://github.com/sonos/dinghy/pull/122) ([kafji](https://github.com/kafji)) ## [0.4.39](https://github.com/sonos/dinghy/tree/0.4.39) (2020-08-05) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.38...0.4.39) **Merged pull requests:** - iOS environment variable support [\#120](https://github.com/sonos/dinghy/pull/120) ([Jasper-Bekkers](https://github.com/Jasper-Bekkers)) - Add arm64e support [\#119](https://github.com/sonos/dinghy/pull/119) ([Jasper-Bekkers](https://github.com/Jasper-Bekkers)) ## [0.4.38](https://github.com/sonos/dinghy/tree/0.4.38) (2020-06-18) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.37...0.4.38) **Merged pull requests:** - Ssh arg port order [\#118](https://github.com/sonos/dinghy/pull/118) ([kali](https://github.com/kali)) - Features bugfix [\#117](https://github.com/sonos/dinghy/pull/117) ([kali](https://github.com/kali)) ## [0.4.37](https://github.com/sonos/dinghy/tree/0.4.37) (2020-05-29) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.35...0.4.37) **Closed issues:** - Long term solution for URL regression [\#101](https://github.com/sonos/dinghy/issues/101) **Merged pull requests:** - add missing x86\_64 android device [\#115](https://github.com/sonos/dinghy/pull/115) ([MarcTreySonos](https://github.com/MarcTreySonos)) - experimental shell expansion in run [\#114](https://github.com/sonos/dinghy/pull/114) ([kali](https://github.com/kali)) - avoid createing huge and useless PKG\_CONFIG\_LIBDIR [\#113](https://github.com/sonos/dinghy/pull/113) ([kali](https://github.com/kali)) - Allow SSH devices to use the host toolchain [\#112](https://github.com/sonos/dinghy/pull/112) ([fredszaq](https://github.com/fredszaq)) ## [0.4.35](https://github.com/sonos/dinghy/tree/0.4.35) (2020-04-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.34...0.4.35) **Merged pull requests:** - bump cargo, and switch to anyhow [\#111](https://github.com/sonos/dinghy/pull/111) ([kali](https://github.com/kali)) - eradicate two warnings [\#108](https://github.com/sonos/dinghy/pull/108) ([kali](https://github.com/kali)) ## [0.4.34](https://github.com/sonos/dinghy/tree/0.4.34) (2020-04-07) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.33...0.4.34) ## [0.4.33](https://github.com/sonos/dinghy/tree/0.4.33) (2020-04-07) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.31...0.4.33) **Merged pull requests:** - build bench in release mode [\#110](https://github.com/sonos/dinghy/pull/110) ([kali](https://github.com/kali)) - some android auto target fixes [\#91](https://github.com/sonos/dinghy/pull/91) ([kali](https://github.com/kali)) ## [0.4.31](https://github.com/sonos/dinghy/tree/0.4.31) (2020-04-07) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.30...0.4.31) ## [0.4.30](https://github.com/sonos/dinghy/tree/0.4.30) (2020-04-06) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.29...0.4.30) ## [0.4.29](https://github.com/sonos/dinghy/tree/0.4.29) (2020-04-06) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.28...0.4.29) ## [0.4.28](https://github.com/sonos/dinghy/tree/0.4.28) (2020-04-06) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.27...0.4.28) ## [0.4.27](https://github.com/sonos/dinghy/tree/0.4.27) (2020-04-06) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.26...0.4.27) ## [0.4.26](https://github.com/sonos/dinghy/tree/0.4.26) (2020-04-06) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.25...0.4.26) ## [0.4.25](https://github.com/sonos/dinghy/tree/0.4.25) (2020-04-06) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.24...0.4.25) **Merged pull requests:** - Daxpedia windows fix [\#107](https://github.com/sonos/dinghy/pull/107) ([kali](https://github.com/kali)) - Fix targets which does not have sysroot [\#106](https://github.com/sonos/dinghy/pull/106) ([MarcTreySonos](https://github.com/MarcTreySonos)) - add upload rsync binary step for the ssh devices [\#105](https://github.com/sonos/dinghy/pull/105) ([MarcTreySonos](https://github.com/MarcTreySonos)) - Android ndk 19+ documentation [\#86](https://github.com/sonos/dinghy/pull/86) ([Deluvi](https://github.com/Deluvi)) ## [0.4.24](https://github.com/sonos/dinghy/tree/0.4.24) (2020-02-13) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.23...0.4.24) **Merged pull requests:** - libc++\_shared.so must be included for android deployment [\#103](https://github.com/sonos/dinghy/pull/103) ([MarcTreySonos](https://github.com/MarcTreySonos)) ## [0.4.23](https://github.com/sonos/dinghy/tree/0.4.23) (2020-02-13) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.22...0.4.23) **Fixed bugs:** - Revert `url` lib bump to 2.1.0 [\#104](https://github.com/sonos/dinghy/pull/104) ([Deluvi](https://github.com/Deluvi)) ## [0.4.22](https://github.com/sonos/dinghy/tree/0.4.22) (2020-01-30) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.21...0.4.22) **Closed issues:** - dinghy\_test::test\_file\_path doesn't work when running tests natively on desktop platform [\#99](https://github.com/sonos/dinghy/issues/99) **Merged pull requests:** - fix ci [\#102](https://github.com/sonos/dinghy/pull/102) ([kali](https://github.com/kali)) - use crate url 2.0.0 [\#97](https://github.com/sonos/dinghy/pull/97) ([MarcTreySonos](https://github.com/MarcTreySonos)) ## [0.4.21](https://github.com/sonos/dinghy/tree/0.4.21) (2020-01-27) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.20...0.4.21) **Merged pull requests:** - Refactor bindgen related code [\#98](https://github.com/sonos/dinghy/pull/98) ([Deluvi](https://github.com/Deluvi)) ## [0.4.20](https://github.com/sonos/dinghy/tree/0.4.20) (2020-01-16) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.19...0.4.20) **Merged pull requests:** - Change IosSimDevice to use xcrun simctl launch rather than lldb [\#96](https://github.com/sonos/dinghy/pull/96) ([simlay](https://github.com/simlay)) ## [0.4.19](https://github.com/sonos/dinghy/tree/0.4.19) (2020-01-13) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.18...0.4.19) **Merged pull requests:** - Updated dependencies [\#95](https://github.com/sonos/dinghy/pull/95) ([simlay](https://github.com/simlay)) ## [0.4.18](https://github.com/sonos/dinghy/tree/0.4.18) (2019-10-28) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.17...0.4.18) **Merged pull requests:** - add a "private" framework search path [\#94](https://github.com/sonos/dinghy/pull/94) ([kali](https://github.com/kali)) ## [0.4.17](https://github.com/sonos/dinghy/tree/0.4.17) (2019-10-24) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.16...0.4.17) ## [0.4.16](https://github.com/sonos/dinghy/tree/0.4.16) (2019-09-16) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.15...0.4.16) ## [0.4.15](https://github.com/sonos/dinghy/tree/0.4.15) (2019-09-16) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.14...0.4.15) **Merged pull requests:** - try on bionic [\#93](https://github.com/sonos/dinghy/pull/93) ([kali](https://github.com/kali)) ## [0.4.14](https://github.com/sonos/dinghy/tree/0.4.14) (2019-09-16) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.13...0.4.14) ## [0.4.13](https://github.com/sonos/dinghy/tree/0.4.13) (2019-09-08) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.12...0.4.13) ## [0.4.12](https://github.com/sonos/dinghy/tree/0.4.12) (2019-08-29) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.11...0.4.12) ## [0.4.11](https://github.com/sonos/dinghy/tree/0.4.11) (2019-05-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.10...0.4.11) **Closed issues:** - Add android app platform version to specify/match a minSdk Version [\#85](https://github.com/sonos/dinghy/issues/85) **Merged pull requests:** - bump osx [\#90](https://github.com/sonos/dinghy/pull/90) ([kali](https://github.com/kali)) - Prevent library copy of lib file in the toolchain's sysroot when running on a remote device [\#88](https://github.com/sonos/dinghy/pull/88) ([Deluvi](https://github.com/Deluvi)) - Make automatic platform decision for a given device deterministic [\#84](https://github.com/sonos/dinghy/pull/84) ([Deluvi](https://github.com/Deluvi)) ## [0.4.10](https://github.com/sonos/dinghy/tree/0.4.10) (2019-02-26) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.8...0.4.10) **Closed issues:** - Fails to sign when there is any non .mobileprovision file in Profiles folder [\#77](https://github.com/sonos/dinghy/issues/77) **Merged pull requests:** - Android ndk 19+ API level choice [\#83](https://github.com/sonos/dinghy/pull/83) ([Deluvi](https://github.com/Deluvi)) - Fix multiple versions of the same dynamic lib used [\#82](https://github.com/sonos/dinghy/pull/82) ([Deluvi](https://github.com/Deluvi)) ## [0.4.8](https://github.com/sonos/dinghy/tree/0.4.8) (2019-02-14) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.7...0.4.8) **Merged pull requests:** - reactivate toolchain binary shims as they are needed by some projects [\#79](https://github.com/sonos/dinghy/pull/79) ([fredszaq](https://github.com/fredszaq)) ## [0.4.7](https://github.com/sonos/dinghy/tree/0.4.7) (2019-02-13) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.6...0.4.7) ## [0.4.6](https://github.com/sonos/dinghy/tree/0.4.6) (2019-02-12) [Full Changelog](https://github.com/sonos/dinghy/compare/0.4.5...0.4.6) **Merged pull requests:** - various fix for ios [\#78](https://github.com/sonos/dinghy/pull/78) ([kali](https://github.com/kali)) ## [0.4.5](https://github.com/sonos/dinghy/tree/0.4.5) (2019-02-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.16...0.4.5) **Closed issues:** - Failure linking backtrace-sys on android: undefined reference to 'getpagesize' [\#61](https://github.com/sonos/dinghy/issues/61) - Error when I try to use dinghy [\#48](https://github.com/sonos/dinghy/issues/48) - Should benchmarks return performance numbers? [\#44](https://github.com/sonos/dinghy/issues/44) - MacOS: Unexpected end of JSON with Xcode CommandLineTools [\#32](https://github.com/sonos/dinghy/issues/32) **Merged pull requests:** - more android fixes and debug info [\#76](https://github.com/sonos/dinghy/pull/76) ([kali](https://github.com/kali)) - refactor + android clang support [\#75](https://github.com/sonos/dinghy/pull/75) ([kali](https://github.com/kali)) - Script device [\#74](https://github.com/sonos/dinghy/pull/74) ([kali](https://github.com/kali)) - update cargo to 0.32 - edition 2018 support [\#73](https://github.com/sonos/dinghy/pull/73) ([fredszaq](https://github.com/fredszaq)) - Fix typos in documentation [\#69](https://github.com/sonos/dinghy/pull/69) ([adrienball](https://github.com/adrienball)) - support --no-run [\#67](https://github.com/sonos/dinghy/pull/67) ([kali](https://github.com/kali)) - filename conflict in "run" [\#65](https://github.com/sonos/dinghy/pull/65) ([kali](https://github.com/kali)) - some trace! level info, plus move exe to target/ in bundle [\#64](https://github.com/sonos/dinghy/pull/64) ([kali](https://github.com/kali)) - Bump dependencies [\#63](https://github.com/sonos/dinghy/pull/63) ([Eijebong](https://github.com/Eijebong)) - add static linking helper [\#62](https://github.com/sonos/dinghy/pull/62) ([MarcTreySonos](https://github.com/MarcTreySonos)) - Define defualt toolchain directory in .dinghy [\#60](https://github.com/sonos/dinghy/pull/60) ([rtmvc](https://github.com/rtmvc)) - do not copy target in target [\#58](https://github.com/sonos/dinghy/pull/58) ([kali](https://github.com/kali)) - Update build\_env.rs [\#57](https://github.com/sonos/dinghy/pull/57) ([warent](https://github.com/warent)) - Update dinghy crate to cargo-dinghy [\#56](https://github.com/sonos/dinghy/pull/56) ([nebuto](https://github.com/nebuto)) - Allow debug build mode arg [\#55](https://github.com/sonos/dinghy/pull/55) ([rtmvc](https://github.com/rtmvc)) - Set permissions before copy [\#54](https://github.com/sonos/dinghy/pull/54) ([rtmvc](https://github.com/rtmvc)) - Copy libs for host platform too [\#53](https://github.com/sonos/dinghy/pull/53) ([rtmvc](https://github.com/rtmvc)) - Warn if package filtered on platform [\#52](https://github.com/sonos/dinghy/pull/52) ([rtmvc](https://github.com/rtmvc)) - Copy ios libs [\#51](https://github.com/sonos/dinghy/pull/51) ([rtmvc](https://github.com/rtmvc)) - Copy .so dependencies in target directory for builds [\#50](https://github.com/sonos/dinghy/pull/50) ([rtmvc](https://github.com/rtmvc)) - Strip executable copy and not original as cargo might not regenerate … [\#49](https://github.com/sonos/dinghy/pull/49) ([rtmvc](https://github.com/rtmvc)) - Copy .so dependencies in target directory for builds [\#47](https://github.com/sonos/dinghy/pull/47) ([kali](https://github.com/kali)) - Strip [\#46](https://github.com/sonos/dinghy/pull/46) ([kali](https://github.com/kali)) - Fix nightly compiler error [\#45](https://github.com/sonos/dinghy/pull/45) ([pitdicker](https://github.com/pitdicker)) - Task/cc rs compat [\#42](https://github.com/sonos/dinghy/pull/42) ([kali](https://github.com/kali)) - 0.3 [\#40](https://github.com/sonos/dinghy/pull/40) ([rtmvc](https://github.com/rtmvc)) ## [0.2.16](https://github.com/sonos/dinghy/tree/0.2.16) (2018-02-05) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.15...0.2.16) **Closed issues:** - Android emulator support? [\#37](https://github.com/sonos/dinghy/issues/37) - Error installing dinghy [\#36](https://github.com/sonos/dinghy/issues/36) ## [0.2.15](https://github.com/sonos/dinghy/tree/0.2.15) (2017-11-23) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.14...0.2.15) **Closed issues:** - MacOS Cannot build with Xcode 9 \(9A235\) [\#29](https://github.com/sonos/dinghy/issues/29) **Merged pull requests:** - Update to cargo 0.22, fix a few warnings [\#33](https://github.com/sonos/dinghy/pull/33) ([ryanpresciense](https://github.com/ryanpresciense)) ## [0.2.14](https://github.com/sonos/dinghy/tree/0.2.14) (2017-10-05) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.13...0.2.14) **Closed issues:** - Refactoring some common logic into a library [\#4](https://github.com/sonos/dinghy/issues/4) **Merged pull requests:** - Bump to xcode 9 [\#31](https://github.com/sonos/dinghy/pull/31) ([klefevre](https://github.com/klefevre)) - Bump dependencies [\#30](https://github.com/sonos/dinghy/pull/30) ([klefevre](https://github.com/klefevre)) ## [0.2.13](https://github.com/sonos/dinghy/tree/0.2.13) (2017-07-04) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.12...0.2.13) ## [0.2.12](https://github.com/sonos/dinghy/tree/0.2.12) (2017-07-04) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.11...0.2.12) **Closed issues:** - Allow specifying port number for ssh [\#26](https://github.com/sonos/dinghy/issues/26) - Need some way of deploying test\_data even if they are included in .gitignore [\#24](https://github.com/sonos/dinghy/issues/24) **Merged pull requests:** - Bump dependencies [\#28](https://github.com/sonos/dinghy/pull/28) ([klefevre](https://github.com/klefevre)) - Implement clean\_app in ssh, add path and port options [\#27](https://github.com/sonos/dinghy/pull/27) ([azdlowry](https://github.com/azdlowry)) - Allow copy\_git\_ignored to be added to test data [\#25](https://github.com/sonos/dinghy/pull/25) ([azdlowry](https://github.com/azdlowry)) - Android Improvements [\#23](https://github.com/sonos/dinghy/pull/23) ([azdlowry](https://github.com/azdlowry)) ## [0.2.11](https://github.com/sonos/dinghy/tree/0.2.11) (2017-06-09) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.10...0.2.11) **Merged pull requests:** - add DINGHY=1 env var when running on Android [\#22](https://github.com/sonos/dinghy/pull/22) ([fredszaq](https://github.com/fredszaq)) ## [0.2.10](https://github.com/sonos/dinghy/tree/0.2.10) (2017-05-29) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.9...0.2.10) **Merged pull requests:** - More options [\#21](https://github.com/sonos/dinghy/pull/21) ([klefevre](https://github.com/klefevre)) ## [0.2.9](https://github.com/sonos/dinghy/tree/0.2.9) (2017-04-21) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.8...0.2.9) **Closed issues:** - make\_linux\_app is creates more and more files each time [\#19](https://github.com/sonos/dinghy/issues/19) **Merged pull requests:** - support more android architechtures [\#20](https://github.com/sonos/dinghy/pull/20) ([dten](https://github.com/dten)) ## [0.2.8](https://github.com/sonos/dinghy/tree/0.2.8) (2017-04-21) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.7...0.2.8) ## [0.2.7](https://github.com/sonos/dinghy/tree/0.2.7) (2017-04-18) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.6...0.2.7) **Merged pull requests:** - Automatic versioning from Cargo.toml [\#18](https://github.com/sonos/dinghy/pull/18) ([klefevre](https://github.com/klefevre)) ## [0.2.6](https://github.com/sonos/dinghy/tree/0.2.6) (2017-04-18) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.5...0.2.6) **Closed issues:** - consider hiding scp/rsyinc output [\#16](https://github.com/sonos/dinghy/issues/16) - paths for .dinghy.toml [\#14](https://github.com/sonos/dinghy/issues/14) ## [0.2.5](https://github.com/sonos/dinghy/tree/0.2.5) (2017-04-15) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.4...0.2.5) **Closed issues:** - run cargo examples? [\#11](https://github.com/sonos/dinghy/issues/11) **Merged pull requests:** - Workspace test data [\#17](https://github.com/sonos/dinghy/pull/17) ([kali](https://github.com/kali)) ## [0.2.4](https://github.com/sonos/dinghy/tree/0.2.4) (2017-04-11) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.3...0.2.4) **Merged pull requests:** - Add spec support for build, test and bench subcommands [\#15](https://github.com/sonos/dinghy/pull/15) ([klefevre](https://github.com/klefevre)) ## [0.2.3](https://github.com/sonos/dinghy/tree/0.2.3) (2017-03-31) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.2...0.2.3) **Merged pull requests:** - fix ssh target triple that was hardcoded instead of read from config [\#12](https://github.com/sonos/dinghy/pull/12) ([fredszaq](https://github.com/fredszaq)) ## [0.2.2](https://github.com/sonos/dinghy/tree/0.2.2) (2017-03-31) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.1...0.2.2) ## [0.2.1](https://github.com/sonos/dinghy/tree/0.2.1) (2017-03-29) [Full Changelog](https://github.com/sonos/dinghy/compare/0.2.0...0.2.1) **Merged pull requests:** - chmod from windows needs fixing [\#13](https://github.com/sonos/dinghy/pull/13) ([dten](https://github.com/dten)) ## [0.2.0](https://github.com/sonos/dinghy/tree/0.2.0) (2017-02-10) [Full Changelog](https://github.com/sonos/dinghy/compare/0.1.13...0.2.0) **Merged pull requests:** - Ssh support [\#10](https://github.com/sonos/dinghy/pull/10) ([kali](https://github.com/kali)) - make dinghy work on linux [\#9](https://github.com/sonos/dinghy/pull/9) ([fredszaq](https://github.com/fredszaq)) ## [0.1.13](https://github.com/sonos/dinghy/tree/0.1.13) (2017-01-31) [Full Changelog](https://github.com/sonos/dinghy/compare/0.1.12...0.1.13) **Merged pull requests:** - Support for multiple travis configurations [\#8](https://github.com/sonos/dinghy/pull/8) ([kali](https://github.com/kali)) ## [0.1.12](https://github.com/sonos/dinghy/tree/0.1.12) (2017-01-26) [Full Changelog](https://github.com/sonos/dinghy/compare/0.1.11...0.1.12) ## [0.1.11](https://github.com/sonos/dinghy/tree/0.1.11) (2017-01-26) [Full Changelog](https://github.com/sonos/dinghy/compare/0.1.10...0.1.11) **Closed issues:** - unresolved name `ensure_shim` when trying to cargo install [\#5](https://github.com/sonos/dinghy/issues/5) **Merged pull requests:** - accept any android device name that isn't whitespace [\#7](https://github.com/sonos/dinghy/pull/7) ([dten](https://github.com/dten)) ## [0.1.10](https://github.com/sonos/dinghy/tree/0.1.10) (2017-01-24) [Full Changelog](https://github.com/sonos/dinghy/compare/0.1.9...0.1.10) ## [0.1.9](https://github.com/sonos/dinghy/tree/0.1.9) (2017-01-23) [Full Changelog](https://github.com/sonos/dinghy/compare/0.1.8...0.1.9) ## [0.1.8](https://github.com/sonos/dinghy/tree/0.1.8) (2017-01-23) [Full Changelog](https://github.com/sonos/dinghy/compare/0.1.7...0.1.8) ## [0.1.7](https://github.com/sonos/dinghy/tree/0.1.7) (2017-01-23) [Full Changelog](https://github.com/sonos/dinghy/compare/0.1.6...0.1.7) ## [0.1.6](https://github.com/sonos/dinghy/tree/0.1.6) (2017-01-18) [Full Changelog](https://github.com/sonos/dinghy/compare/0.1.5...0.1.6) **Closed issues:** - Link error when executing 'cargo install dinghy' [\#3](https://github.com/sonos/dinghy/issues/3) ## [0.1.5](https://github.com/sonos/dinghy/tree/0.1.5) (2017-01-03) [Full Changelog](https://github.com/sonos/dinghy/compare/0.1.4...0.1.5) ## [0.1.4](https://github.com/sonos/dinghy/tree/0.1.4) (2016-12-16) [Full Changelog](https://github.com/sonos/dinghy/compare/29e2f6c0b21a11575a4af2a9aba993ab3e2bf549...0.1.4) **Merged pull requests:** - missing env error message is wrong [\#2](https://github.com/sonos/dinghy/pull/2) ([dten](https://github.com/dten)) - Windows build [\#1](https://github.com/sonos/dinghy/pull/1) ([dten](https://github.com/dten)) \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "2" members = [ "cargo-dinghy", "dinghy-build", "dinghy-lib", "dinghy-test", ] [workspace.dependencies] cargo_metadata = "=0.14.0" cargo-platform = "=0.1.8" ================================================ FILE: LICENSE ================================================ ## License Licensed under either of * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. ================================================ FILE: LICENSE-APACHE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: LICENSE-MIT ================================================ 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. ================================================ FILE: README.md ================================================ # Dinghy ![rustc >= 1.85.0](https://img.shields.io/badge/rustc-%3E%3D1.85.0-brightgreen) ![MIT/Apache 2](https://img.shields.io/crates/l/dinghy) ![Build and test](https://github.com/sonos/dinghy/workflows/Build%20and%20test/badge.svg) ## What? Dinghy is a `cargo` extension to bring cargo workflow to cross-compilation situations. Dinghy is specifically useful with "small" processor-based devices, like Android and iOS phones, or small single board computers like the Raspberry Pi. Situations where native compilation is not possible, or not practical. Initially tests and benches were the primary objective of Dinghy, but now at Snips we use it to cross-compile our entire platform. This includes setting up the stage for `cc` and `pkg-config` crates in one single place. If you are a Rust library author, **you can run your tests and benches on your smartphone in minutes.** And you should, at least once in a while. ## Demo Let's try how BurntSushi's byteorder handles f32 on a few arm devices, two smartphones, and a Raspberry Pi. ![Demo](docs/demo.gif) Phew. It works. ## How? Once dinghy knows about your toolchains and devices, you will be able to run tests and benches from a simple cargo command **in any cargo project**, most of the time without altering them. Just add `dinghy -d some_device` between `cargo` and its subcommand: ``` cargo dinghy -d my_android test cargo dinghy -d my_raspberry bench ``` By default, without `-d`, Dinghy will make a native build, just like `cargo` would do. ## Getting started Depending on your targets and your workstation, the ease of setting up Dinghy can vary. * [Android](docs/android.md) is relatively easy, specifically if you already are a mobile developer. * [iOS](docs/ios.md) setup has a lot of steps, but at least Apple provides everything you will need. Once again, if you are an iOS developer, most of the heavy lifting has been already done. And if you are not, be aware that you won't have to pay anything. * [other remote ssh-accessible devices](docs/ssh.md) are the easiest from dinghy point of view, but you will be on your own to obtain the toolchain for your device architecture and operating system. If your device is a Raspberry Pi running raspbian, we can help. :) ## Advanced topics and features * Some projects need [resources files](docs/files.md) for running their tests or benches. Dinghy tries its best to make it work in as many project/target configurations as possible but some projects need a bit of help. * In some bigger projects, you may need to [filter](docs/filter.md) your project's members depending on the platform you want to test. * Passing [environment](docs/vars.md) variables to your executable may sometimes be useful. * Dinghy offers an [overlay](docs/overlay.md) system to "add" stuff to your toolchain sysroot. This allows you to add "stuff" to your build dependencies, like static libraries or headers without altering the sysroot toolchain. * The [`dinghy-build` crate](docs/dinghy-build.md) offers some `build.rs` features that are useful in the context of cross-compilation. ## Using dinghy as a runner only If your project already build for the target platform without dinghy and you only want to use dinghy to run code on a device, you can use dinghy's bundled runner directly. You simply need to register the dinghy as a runner in `.cargo/config`. Here's an example for all apple targets ```toml [target.'cfg(all(any(target_arch="aarch64",target_arch="x86_64"),target_vendor="apple",any(target_os="ios",target_os="tvos",target_os="apple-watchos")))'] runner = "cargo dinghy runner --" ``` You can then run your tests directly with `cargo test --target aarch64-apple-ios-sim` for example. Please note that the recommended way to use dinghy is as a cargo subcommand as it will set up quite a few things automatically for your project to even build. The runner will try to auto-detect the platform if it is not passed (as in the above example) # License Licensed under either of * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ## Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. ================================================ FILE: cargo-dinghy/Cargo.toml ================================================ [package] name = "cargo-dinghy" version = "0.8.5-pre" authors = ["Mathieu Poumeyrol ", "Thibaut Lorrain "] license = "MIT/Apache-2.0" description = "Cross-compilation made easier" homepage = "https://medium.com/snips-ai/dinghy-painless-rust-tests-and-benches-on-ios-and-android-c9f94f81d305#.c2sx7two8" repository = "https://github.com/sonos/dinghy" keywords = [ "tests", "mobile", "ios", "android", "cargo" ] categories = [ "development-tools::cargo-plugins", "development-tools::testing" , "development-tools::profiling" ] readme = "../README.md" edition = "2021" [badges] travis-ci = { repository = "sonos/dinghy" } [dependencies] dinghy-lib = { path = "../dinghy-lib" } log = "0.4" clap = { version = "4", features = ["derive"] } env_logger = "0.10" anyhow = "1.0.57" cargo_metadata.workspace=true ================================================ FILE: cargo-dinghy/src/cli.rs ================================================ use clap::{CommandFactory, Parser, Subcommand}; use std::collections::HashSet; #[derive(Parser, Debug, Clone)] #[command(author, version, about, long_about = None)] pub struct DinghyGeneralArgs { /// Use a specific platform #[arg(long, short)] pub platform: Option, /// Make output more verbose, can be passed multiple times #[arg(long, short, action = clap::ArgAction::Count)] pub verbose: u8, /// Make output less verbose, can be passed multiple times #[arg(long, short, action = clap::ArgAction::Count)] pub quiet: u8, /// Force the use of an overlay during project build, can be passed multiple times #[arg(long, short)] pub overlay: Vec, /// Env variables to set on target device e.g. RUST_TRACE=trace, can be passed multiple times #[arg(long, short)] pub env: Vec, /// Cleanup target device after completion #[arg(long, short)] pub cleanup: bool, /// Strip executable before running it on target #[arg(long, short)] pub strip: bool, /// Device hint #[arg(long, short)] pub device: Option, /// Either a dinghy subcommand (see cargo dinghy all-dinghy-subcommands) or a /// cargo one (see cargo --list) // this one is here so that the help generated by clap makes sense pub subcommand: Vec, } #[derive(Parser, Debug)] pub struct SubCommandWrapper { #[command(subcommand)] subcommand: DinghySubcommand, } #[derive(Subcommand, Debug)] pub enum DinghySubcommand { /// List devices that can be used with Dinghy for the selected platform Devices {}, /// List all devices that can be used with Dinghy AllDevices {}, /// List all platforms known to dinghy AllPlatforms {}, /// List all available dinghy subcommands AllDinghySubcommands {}, /// Dinghy runner, used internally to run executables on targets Runner { args: Vec }, /// Build an artifact and run it on a target device using the provided wrapper RunWith { /// Wrapper crate to use to run the lib #[arg(long, short('c'))] wrapper_crate: String, // TODO support executables / scripts as wrappers // /// Wrapper executable to use to run the lib // #[clap(long, short('e'))] // wrapper_executable: Option, /// Arguments to cargo build for the artifact lib_build_args: Vec, }, } #[derive(Debug)] pub enum DinghyMode { DinghySubcommand(DinghySubcommand), CargoSubcommand { args: Vec }, Naked, } #[derive(Debug)] pub struct DinghyCli { pub args: DinghyGeneralArgs, pub mode: DinghyMode, } impl DinghyCli { pub fn parse() -> Self { log::debug!("args {:?}", std::env::args().collect::>()); let args = std::env::args().skip(1).skip_while(|it| it == "dinghy"); #[derive(Debug, Default)] struct SplitArgs { general_args: Vec, subcommand: Vec, } let args_taking_value = DinghyGeneralArgs::command() .get_arguments() .filter_map(|arg| { if arg.get_value_names().is_some() { let mut values = vec![]; if let Some(shorts) = arg.get_short_and_visible_aliases() { values .append(&mut shorts.iter().map(|short| format!("-{}", short)).collect()) } if let Some(longs) = arg.get_long_and_visible_aliases() { values.append(&mut longs.iter().map(|long| format!("--{}", long)).collect()) } if values.is_empty() { None } else { Some(values) } } else { None } }) .flatten() .collect::>(); let split_args = args.fold(SplitArgs::default(), |mut split_args, elem| { if !split_args.subcommand.is_empty() { // we've started putting args in the sub command, let's continue split_args.subcommand.push(elem) } else { if elem.starts_with("-") /* This is a new option */ || split_args.general_args .last() .map(|it| args_taking_value.contains(it)) .unwrap_or(false) /* value for the previous option */ { split_args.general_args.push(elem) } else { // leve the start of the subcommand here so that clap can verify it is there split_args.general_args.push(elem.clone()); // this is the start of the sub command split_args.subcommand.push(elem) } } split_args }); let cli = DinghyCli { args: Parser::parse_from( vec!["dinghy".to_string()] .into_iter() .chain(split_args.general_args), ), mode: split_args .subcommand .first() .cloned() .map(|subcommand| { if DinghySubcommand::has_subcommand(&subcommand) { DinghyMode::DinghySubcommand( SubCommandWrapper::parse_from( vec!["dinghy".to_string()] .into_iter() .chain(split_args.subcommand), ) .subcommand, ) } else { DinghyMode::CargoSubcommand { args: split_args.subcommand, } } }) .unwrap_or(DinghyMode::Naked), }; log::debug!("cli {:?}", cli); cli } } ================================================ FILE: cargo-dinghy/src/main.rs ================================================ use std::convert::identity; use std::env; use std::env::current_dir; use std::io::BufReader; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::Arc; use cargo_metadata::camino::Utf8PathBuf; use cargo_metadata::Message; use log::{debug, error, info}; use dinghy_lib::config::dinghy_config; use dinghy_lib::errors::*; use dinghy_lib::project::Project; use dinghy_lib::utils::{set_current_verbosity, user_facing_log, LogCommandExt}; use dinghy_lib::Dinghy; use dinghy_lib::Platform; use dinghy_lib::{Build, SetupArgs}; use dinghy_lib::{Device, Runnable}; use crate::cli::{DinghyCli, DinghyMode, DinghySubcommand, SubCommandWrapper}; mod cli; fn main() { env_logger::init_from_env( env_logger::Env::new() .filter("DINGHY_LOG") .write_style("DINGHY_LOG_STYLE"), ); let cli = DinghyCli::parse(); set_current_verbosity(cli.args.verbose as i8); if let Err(e) = run_command(cli) { error!("{:?}", e); // positively ugly. if e.to_string().contains("are filtered out on platform") { std::process::exit(3) } else { std::process::exit(1) } } } fn run_command(cli: DinghyCli) -> Result<()> { let conf = Arc::new(dinghy_config(current_dir()?)?); let metadata = cargo_metadata::MetadataCommand::new().exec()?; let project = Project::new(&conf, metadata); let dinghy = Dinghy::probe(&conf)?; let (platform, device) = select_platform_and_device_from_cli(&cli, &dinghy)?; let setup_args = SetupArgs { verbosity: cli.args.verbose as i8 - cli.args.quiet as i8, forced_overlays: cli.args.overlay.clone(), envs: cli.args.env.clone(), cleanup: cli.args.cleanup, strip: cli.args.strip, // TODO this should probably be configurable in the config as well device_id: device.as_ref().map(|d| d.id().to_string()), }; match cli.mode { DinghyMode::CargoSubcommand { ref args } => { let mut cmd = create_cargo_subcomand(&platform, &device, &project, &setup_args, args)?; log::debug!("Launching {:?}", cmd); let status = cmd.log_invocation(2).status()?; log::debug!("done"); std::process::exit(status.code().unwrap_or_else(|| { log::error!("Could not get cargo exit code"); -1 })); } DinghyMode::DinghySubcommand(DinghySubcommand::Runner { ref args }) => { debug!("starting dinghy runner, args {:?}", args); let exe = args.first().cloned().unwrap(); let exe_path = PathBuf::from(&exe); let inferred_target = exe_path .parent() // build type .and_then(|path| { if path .file_name() .map(|name| name.to_string_lossy() == "deps") .unwrap_or(false) { path.parent() } else { Some(path) } }) .and_then(Path::parent) // either "target" for the host or the actual target name if we're cross compiling .and_then(Path::file_name) .map(|it| it.to_string_lossy()); debug!("inferred target {:?}", inferred_target); let (mut final_platform, mut final_device) = (platform, device); if let Some(inferred_target) = inferred_target { if final_device.is_none() && final_platform.rustc_triple() != inferred_target && cli.args.platform.is_none() { let platform = dinghy .platforms() .into_iter() .find(|p| p.rustc_triple() == inferred_target); if let Some(platform) = platform { let device = find_first_device_for_platform(&cli, &dinghy, &platform); if let Some(device) = device { info!("Runner was called without explicit platform, we found {} and device {}", platform.id(), device.id()); final_device = Some(device) } final_platform = platform; } } }; if let Some(device) = final_device { user_facing_log( "Targeting", &format!( "platform {} and device {}", final_platform.id(), device.id() ), 0, ); let exe_name = exe_path.file_name().unwrap().to_str().unwrap().to_string(); let exe_id = if exe_name == "rust_out" { // rustdoc may run concurent runners all with the same exe name, fortunately the parent dir is // different in that case so lets use that instead as an id exe_path.parent().unwrap().file_name().unwrap().to_str().unwrap().to_string() } else { exe_name }; let (args, files_in_run_args): (Vec, Vec>) = args .into_iter() .skip(1) .map(|arg| { if arg.contains(std::path::MAIN_SEPARATOR) { let path_buf = PathBuf::from(&arg); if path_buf.exists() { ( PathBuf::from(".") .join(path_buf.file_name().unwrap()) .to_str() .unwrap() .to_string(), Some(path_buf), ) } else { (arg.clone(), None) } } else { (arg.clone(), None) } }) .unzip(); let files_in_run_args = files_in_run_args.into_iter().filter_map(identity).collect(); let args_ref = args.iter().map(|s| &s[..]).collect::>(); let envs_ref = cli.args.env.iter().map(|s| &s[..]).collect::>(); final_platform.setup_env(&project, &setup_args)?; let mut build = Build { setup_args, // TODO these should be probably read from the executable file dynamic_libraries: vec![], runnable: Runnable { id: exe_id, package_name: std::env::var("CARGO_PKG_NAME")?, exe: PathBuf::from(exe).canonicalize()?, // cargo launches the runner inside the dir of the crate source: PathBuf::from(".").canonicalize()?, skip_source_copy: conf.skip_source_copy, }, target_path: project.metadata.target_directory.clone().into(), files_in_run_args, }; if cli.args.strip { final_platform.strip(&mut build)?; } let bundle = device.run_app( &project, &build, &args_ref, &envs_ref, // TODO these are also in the SetupArgs )?; // TODO this is not done if the run fails if cli.args.cleanup { device.clean_app(&bundle)?; } } else { bail!("No device for platform {}", final_platform.id()) } Ok(()) } DinghyMode::DinghySubcommand(DinghySubcommand::Devices {}) => { match cli .args .platform .as_ref() .map(|name| dinghy.platform_by_name(name)) { None => anyhow::bail!("No platform provided"), Some(None) => anyhow::bail!("Unknown platform"), Some(Some(platform)) => show_all_devices_for_platform(&dinghy, platform), } } DinghyMode::DinghySubcommand(DinghySubcommand::AllDevices {}) => show_all_devices(&dinghy), DinghyMode::DinghySubcommand(DinghySubcommand::AllPlatforms {}) => { show_all_platforms(&dinghy) } DinghyMode::DinghySubcommand(DinghySubcommand::AllDinghySubcommands {}) => { use clap::CommandFactory; for sub in SubCommandWrapper::command().get_subcommands() { println!( "{}\n\t{}", sub.get_name(), sub.get_about().unwrap_or_default() ); } Ok(()) } DinghyMode::DinghySubcommand(DinghySubcommand::RunWith { wrapper_crate, mut lib_build_args, }) => { let mut build_command = vec!["build".to_string(), "--message-format=json".to_string()]; build_command.append(&mut lib_build_args); let mut build_cargo_cmd = create_cargo_subcomand(&platform, &device, &project, &setup_args, &build_command)?; log::debug!("Launching {:?}", build_cargo_cmd); let mut child = build_cargo_cmd .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .log_invocation(2) .spawn()?; log::debug!("done"); let mut extra_libs = vec![]; let mut lib_file = cargo_metadata::Message::parse_stream(BufReader::new(child.stdout.take().unwrap())) .filter_map(|message| match message { Ok(Message::CompilerArtifact(artifact)) => { if artifact.target.kind.contains(&"dylib".to_string()) { extra_libs.append(&mut artifact.filenames.clone()) } Some(artifact) } Ok(Message::CompilerMessage(message)) => { // TODO would be really nice to get color there but current version of // TODO cargo-metadata doesn't seem to support it eprintln!("{}", message.message); None } Ok(Message::BuildFinished(build)) => { if !build.success { log::debug!("cargo reported a build failure"); } None } Ok(Message::TextLine(text)) => { eprintln!("{}", text); None } _ => None, }) .last() .ok_or_else(|| anyhow!("cargo did not produce an artifact"))? .filenames .into_iter() .next() .ok_or_else(|| anyhow!("no file in cargo artifact"))?; if !extra_libs.is_empty() { // if we have some extra libs, this means we're dealing with a dynamically linked // lib and we need to include the rust stdlib as well, let's try and find it in the // rustup home if let (Ok(rustup_home), Ok(rustup_toolchain)) = ( std::env::var("RUSTUP_HOME"), std::env::var("RUSTUP_TOOLCHAIN"), ) { let stdlib_dir = PathBuf::from(rustup_home) .join("toolchains") .join(rustup_toolchain) .join("lib") .join("rustlib") .join(platform.rustc_triple()) .join("lib"); if let Ok(Some(stdlib)) = stdlib_dir.read_dir().map(|dir| { dir.filter_map(|entry| { entry .and_then(|entry| { entry.file_type().map(|file_type| { if file_type.is_file() && entry .path() .file_stem() .map(|stem| { stem.to_string_lossy().starts_with("libstd-") }) .unwrap_or(false) && entry .path() .extension() .map(|ext| { ["so", "dll", "dylib"] .contains(&ext.to_string_lossy().as_ref()) }) .unwrap_or(false) { Some(entry.path()) } else { None } }) }) .unwrap_or(None) }) .next() }) { if let Ok(stdlib_path_buf) = Utf8PathBuf::from_path_buf(stdlib) { extra_libs.push(stdlib_path_buf) } } } } let code = child.wait()?.code(); match code { Some(0) => { /*expected*/ } Some(c) => std::process::exit(c), None => std::process::exit(-1), } if cli.args.strip { let stripped_dir = lib_file .parent() .ok_or_else(|| anyhow!("failed to get lib dir"))? .join("stripped"); std::fs::create_dir_all(&stripped_dir)?; let stripped_lib_file = stripped_dir.join( lib_file .file_name() .ok_or_else(|| anyhow!("failed to get lib name"))?, ); std::fs::copy(lib_file, &stripped_lib_file)?; let mut lib_build = Build { setup_args: setup_args.clone(), dynamic_libraries: vec![], runnable: Runnable { id: "".to_string(), package_name: "".to_string(), exe: stripped_lib_file.to_path_buf().into(), source: Default::default(), skip_source_copy: conf.skip_source_copy, }, target_path: Default::default(), files_in_run_args: vec![], }; platform.strip(&mut lib_build)?; std::fs::copy(lib_build.runnable.exe, &stripped_lib_file)?; lib_file = stripped_lib_file; } let mut args = vec![ "run".to_string(), "-p".to_string(), wrapper_crate, "--release".to_string(), "--".to_string(), lib_file.to_string(), ]; for extra_lib in extra_libs { args.push(extra_lib.to_string()) } let mut run_cargo_cmd = create_cargo_subcomand(&platform, &device, &project, &setup_args, &args)?; log::debug!("Launching {:?}", run_cargo_cmd); let status = run_cargo_cmd.log_invocation(2).status()?; log::debug!("done"); std::process::exit(status.code().unwrap_or_else(|| { log::error!("Could not get cargo exit code"); -1 })); } DinghyMode::Naked => { anyhow::bail!("Naked mode") // what should we do? } } } fn create_cargo_subcomand( platform: &Arc>, device: &Option>>, project: &Project, setup_args: &SetupArgs, args: &Vec, ) -> Result { info!( "Targeting platform '{}' and device '{}'", platform.id(), device.as_ref().map(|it| it.id()).unwrap_or("") ); user_facing_log( "Targeting", &format!( "platform {} and device {}", platform.id(), device.as_ref().map(|it| it.id()).unwrap_or("") ), 0, ); let cargo = env::var("CARGO") .map(PathBuf::from) .ok() .unwrap_or_else(|| PathBuf::from("cargo")); let mut cmd = Command::new(cargo); for arg in args { cmd.arg(arg); } platform.setup_env(&project, &setup_args)?; Ok(cmd) } fn show_all_platforms(dinghy: &Dinghy) -> Result<()> { let mut platforms = dinghy.platforms(); platforms.sort_by(|str1, str2| str1.id().cmp(&str2.id())); for pf in platforms.iter() { println!("* {} {}", pf.id(), pf.rustc_triple()); } Ok(()) } fn show_all_devices(dinghy: &Dinghy) -> Result<()> { println!("List of available devices for all platforms:"); show_devices(&dinghy, None) } fn show_all_devices_for_platform(dinghy: &Dinghy, platform: Arc>) -> Result<()> { println!( "List of available devices for platform '{}':", platform.id() ); show_devices(&dinghy, Some(platform)) } fn show_devices(dinghy: &Dinghy, platform: Option>>) -> Result<()> { let devices = dinghy .devices() .into_iter() .filter(|device| { platform .as_ref() .map_or(true, |it| it.is_compatible_with(&***device)) }) .collect::>(); if devices.is_empty() { error!("No matching device found"); println!("No matching device found"); } else { for device in devices { let pf: Vec<_> = dinghy .platforms() .iter() .filter(|pf| pf.is_compatible_with(&**device)) .cloned() .collect(); println!("{}: {:?}", device, pf); } } Ok(()) } fn select_platform_and_device_from_cli( cli: &DinghyCli, dinghy: &Dinghy, ) -> Result<(Arc>, Option>>)> { if let Some(platform_name) = cli.args.platform.as_ref() { let platform = dinghy .platform_by_name(platform_name) .ok_or_else(|| anyhow!("No '{}' platform found", platform_name))?; let device = find_first_device_for_platform(cli, dinghy, &platform); Ok((platform, device)) } else if let Some(device_filter) = cli.args.device.as_ref() { let is_banned_auto_platform_id = |id: &str| -> bool { id.contains("auto-android") && (id.contains("min") || id.contains("latest") || id.contains("api")) }; let devices = dinghy .devices() .into_iter() .filter(move |it| { format!("{:?}", it) .to_lowercase() .contains(&device_filter.to_lowercase()) }) .collect::>(); if devices.len() == 0 { bail!("No devices found for name hint `{}'", device_filter) } devices .into_iter() .filter_map(|d| { let pf = dinghy .platforms() .iter() .filter(|pf| !is_banned_auto_platform_id(&pf.id())) .find(|pf| pf.is_compatible_with(&**d)) .cloned(); debug!( "Looking for platform for {}: found {:?}", d.id(), pf.as_ref().map(|p| p.id()) ); pf.map(|it| (it, Some(d))) }) .next() .ok_or_else(|| { anyhow!( "No device and platform combination found for device hint `{}'", device_filter ) }) } else { Ok((dinghy.host_platform(), None)) } } fn find_first_device_for_platform( cli: &DinghyCli, dinghy: &Dinghy, platform: &Arc>, ) -> Option>> { dinghy .devices() .into_iter() .filter(|device| { cli.args .device .as_ref() .map(|filter| { format!("{:?}", device) .to_lowercase() .contains(&filter.to_lowercase()) }) .unwrap_or(true) }) .filter(|it| platform.is_compatible_with(&**it.as_ref())) .next() } ================================================ FILE: dinghy-build/Cargo.toml ================================================ [package] name = "dinghy-build" version = "0.8.5-pre" authors = ["Mathieu Poumeyrol "] license = "MIT/Apache-2.0" description = "Cross-compilation made easier - helpers for build.rs scripts" homepage = "https://medium.com/snips-ai/dinghy-painless-rust-tests-and-benches-on-ios-and-android-c9f94f81d305#.c2sx7two8" repository = "https://github.com/sonos/dinghy" keywords = [ "tests", "mobile", "ios", "android", "cargo" ] categories = [ "development-tools::cargo-plugins", "development-tools::testing" , "development-tools::profiling" ] edition = "2018" [dependencies] anyhow = "1.0.58" cc = "1" log="0.4" ================================================ FILE: dinghy-build/src/bindgen_macros.rs ================================================ /// Create a new `bindgen::Builder` set up and ready for cross-compilation. /// /// This macro should be used for bindgen versions from 0.49 and above. #[macro_export] macro_rules! dinghy_bindgen { () => {{ let bindgen = $crate::dinghy_bindgen_pre_0_49!(); if $crate::build::is_cross_compiling().expect("Couldn't determine if it is cross-compiling") { bindgen.detect_include_paths(false) } else { bindgen } }}; } /// Compatibility macro for bindgen versions below 0.49 #[macro_export] macro_rules! dinghy_bindgen_pre_0_49 { () => {{ use $crate::build::is_cross_compiling; use $crate::build_env::sysroot_path; use $crate::utils::path_to_str; use $crate::{Result, Context}; fn apple_patch(builder: bindgen::Builder) -> Result { if is_cross_compiling()? { let target = env::var("TARGET")?; if target.contains("apple") && target.contains("aarch64") { // The official Apple tools use "-arch arm64" instead of specifying // -target directly; -arch only works when the default target is // Darwin-based to put Clang into "Apple mode" as it were. But it does // sort of explain why arm64 works better than aarch64, which is the // preferred name everywhere else. return Ok(builder .clang_arg(format!("-arch")) .clang_arg(format!("arm64"))); } } Ok(builder) } fn libclang_path_patch(builder: bindgen::Builder) -> Result { if is_cross_compiling()? { if let Ok(libclang_path) = env::var("DINGHY_BUILD_LIBCLANG_PATH") { env::set_var("LIBCLANG_PATH", libclang_path) } } Ok(builder) } fn detect_toolchain(builder: bindgen::Builder) -> Result { if is_cross_compiling()? { let target = env::var("TARGET")?; let builder = if let Ok(_) = env::var("TARGET_SYSROOT") { builder.clang_arg(format!("--sysroot={}", path_to_str(&sysroot_path()?)?)) } else { println!("cargo:warning=No Sysroot detected, assuming the target is baremetal. If you have a sysroot, you must either define a TARGET_SYSROOT or use Dinghy to build your project."); builder }; Ok(builder.clang_arg(format!("--target={}", target))) } else { Ok(builder) } } fn include_gcc_system_headers(builder: bindgen::Builder) -> Result { if is_cross_compiling()? { // Add a path to the private headers for the target compiler. Borderline, // as we are likely using a gcc header with clang frontend. let path = cc::Build::new() .get_compiler() .to_command() .arg("--print-file-name=include") .output() .with_context(|| "Couldn't find target GCC executable.") .and_then(|output| { if output.status.success() { Ok(String::from_utf8(output.stdout)?) } else { panic!("Couldn't determine target GCC include dir.") } })?; Ok(builder.clang_arg("-isystem").clang_arg(path.trim())) } else { Ok(builder) } } libclang_path_patch( apple_patch( include_gcc_system_headers( detect_toolchain(bindgen::Builder::default().clang_arg("--verbose")).unwrap(), ) .unwrap(), ) .unwrap() ) .unwrap() }}; } /// Generate a file containing the bindgen bindings in a standard path. /// /// The standard path is `${OUT_DIR}/bindings.rs`. /// /// To use it, simply perform the call like /// `generate_default_bindgen_bindings!(bindgen_builder)` #[macro_export] macro_rules! generate_bindgen_bindings { ($builder:expr) => {{ let out_path = env::var("OUT_DIR") .map(PathBuf::from) .expect("Couldn't convert OUT_DIR var into a path") .join("bindings.rs"); $builder .generate() .expect("Unable to generate bindings") .write_to_file(out_path) .expect("Unable to write the bindings in the file") }}; } ================================================ FILE: dinghy-build/src/build.rs ================================================ use super::Result; ///! Helpers functions to output `cargo:` lines suitable for build.rs output. use std::env; use std::path::Path; /// Find out if we are cross-compiling. pub fn is_cross_compiling() -> Result { Ok(env::var("TARGET")? != env::var("HOST")?) } /// Adds a `cargo:rustc-link-lib=` line. pub fn include_path>(lib_dir_path: P) -> Result<()> { println!("cargo:rustc-link-lib={}", lib_dir_path.as_ref().display()); Ok(()) } /// Adds a `cargo:rustc-link-search=` and `cargo:rustc-link-lib=static=` line. pub fn link_static>(lib_name: &str, lib_dir_path: P) -> Result<()> { println!( "cargo:rustc-link-search={}", lib_dir_path.as_ref().display() ); println!("cargo:rustc-link-lib=static={}", lib_name); Ok(()) } /// Adds a `cargo:rustc-link-search=` and `cargo:rustc-link-lib=dylib=` line. pub fn link_dylib>(lib_name: &str, lib_dir_path: P) -> Result<()> { println!( "cargo:rustc-link-search={}", lib_dir_path.as_ref().display() ); println!("cargo:rustc-link-lib=dylib={}", lib_name); Ok(()) } /// Adds a `cargo:rustc-link-search` and `cargo:rustc-link-lib=` line. pub fn link_lib>(lib_name: &str, lib_dir_path: P) -> Result<()> { println!( "cargo:rustc-link-search={}", lib_dir_path.as_ref().display() ); println!("cargo:rustc-link-lib={}", lib_name); Ok(()) } /// Adds a `cargo:rustc-link-lib=dylib=` line. pub fn link_system_dylib(lib_name: &str) -> Result<()> { println!("cargo:rustc-link-lib=dylib={}", lib_name); Ok(()) } /// Adds a `cargo:rustc-link-lib=` line. pub fn link_system_lib(lib_name: &str) -> Result<()> { println!("cargo:rustc-link-lib={}", lib_name); Ok(()) } /// Adds a `cargo:rerun-if-changed=` line. pub fn rerun_if_changed>(filepath: P) { println!("cargo:rerun-if-changed={}", filepath.as_ref().display()); } ================================================ FILE: dinghy-build/src/build_env.rs ================================================ //! Target-aware environment manipulations. //! //! cc-rs and pkg-config-rs use a similar convention where some environment //! variables (like CC, CFLAGS or PKG_CONFIG_PATH) can be tagged with the //! current rustc target to distinguish a native build environment and one //! or several cross-compilation ones. //! //! For instance, while compiling for Android arm, `cc-rs` looks first at //! CC_arm-linux-androideabi, then CC_arm_linux_androideabi, the TARGET_CC //! and finally CC. //! //! This crates implements some of the same logic and also helps generating //! these variables names. It also notify all environment lookup "back" to //! cargo using `cargo:rerun-if-env-changed` markup. use anyhow::{Context, Result}; use std::env; use std::ffi::OsStr; use std::ffi::OsString; use std::path::PathBuf; /// Append a value to a PATH-like (`:`-separated) environment variable. pub fn append_path_to_env, V: AsRef>(key: K, value: V) { let mut formatted_value = OsString::new(); if let Ok(initial_value) = env::var(key.as_ref()) { formatted_value.push(initial_value); formatted_value.push(":"); } formatted_value.push(value); env::set_var(key.as_ref(), formatted_value); } /// Append a value to a PATH-like (`:`-separated) environment variable taking /// target scoping rules into consideration. pub fn append_path_to_target_env, R: AsRef, V: AsRef>( k: K, rustc_triple: Option, v: V, ) { append_path_to_env(target_key_from_triple(k, rustc_triple), v.as_ref()) } /// Build-context aware environment variable access. /// /// If we are running in a build.rs context, register the var to cargo using /// `cargo:rerun-if-env-changed`. pub fn build_env(name: &str) -> Result { let is_build_rs = env::var("CARGO_PKG_NAME").is_ok() && env::var("OUT_DIR").is_ok(); if is_build_rs { println!("cargo:rerun-if-env-changed={}", name); } Ok(env::var(name)?) } /// Capitalize and replace `-` by `_`. pub fn envify>(name: S) -> String { name.as_ref() .chars() .map(|c| c.to_ascii_uppercase()) .map(|c| if c == '-' || c == '.' { '_' } else { c }) .collect() } /// Set a bunch of environment variables. pub fn set_all_env, V: AsRef>(env: &[(K, V)]) { for env_var in env { set_env(env_var.0.as_ref(), env_var.1.as_ref()) } } /// Set one environment variable. pub fn set_env, V: AsRef>(k: K, v: V) { log::debug!( "Setting environment variable {:?}={:?}", k.as_ref(), v.as_ref() ); env::set_var(k, v); } /// Set one environment variable if not set yet. pub fn set_env_ifndef, V: AsRef>(k: K, v: V) { if let Ok(current_env_value) = env::var(k.as_ref()) { log::debug!( "Ignoring value {:?} as environment variable {:?} already defined with value {:?}", k.as_ref(), v.as_ref(), current_env_value ); } else { log::debug!( "Setting environment variable {:?}={:?}", k.as_ref(), v.as_ref() ); env::set_var(k, v); } } /// Set one environment variable with target-scoping rules. pub fn set_target_env, R: AsRef, V: AsRef>( k: K, rustc_triple: Option, v: V, ) { set_env(target_key_from_triple(k, rustc_triple), v); } /// Access a required TARGET_SYSROOT variable, suggesting to define it or use /// Dinghy. pub fn sysroot_path() -> Result { env::var_os("TARGET_SYSROOT") .map(PathBuf::from) .context("You must either define a TARGET_SYSROOT or use Dinghy to build your project.") } /// Access `var_base` directly, or use targetting rules depending on the build /// being native or cross. pub fn target_env(var_base: &str) -> Result { if let Ok(target) = env::var("TARGET") { let is_host = env::var("HOST")? == target; target_env_from_triple(var_base, target.as_str(), is_host) } else { build_env(var_base) } } /// Access `var_base` directly, using targetting rules. pub fn target_env_from_triple(var_base: &str, triple: &str, is_host: bool) -> Result { build_env(&format!("{}_{}", var_base, triple)) .or_else(|_| build_env(&format!("{}_{}", var_base, triple.replace("-", "_")))) .or_else(|_| { build_env(&format!( "{}_{}", if is_host { "HOST" } else { "TARGET" }, var_base )) }) .or_else(|_| build_env(var_base)) } fn target_key_from_triple, R: AsRef>( k: K, rustc_triple: Option, ) -> OsString { let mut target_key = OsString::new(); target_key.push(k); if let Some(rustc_triple) = rustc_triple { target_key.push("_"); target_key.push(rustc_triple.as_ref().replace("-", "_")); } target_key } ================================================ FILE: dinghy-build/src/lib.rs ================================================ //! Helpers for build.rs scripts. //! //! This library is meant to be used in build.rs scripts context. //! //! It contains a set of standalone functions that encodes some of the //! shared wisdom and conventions across build.rs scripts, cargo, dinghy, //! cc-rs, pkg-config-rs, bindgen, and others. It also helps providing //! cross-compilation arguments to autotools `./configure` scripts. mod bindgen_macros; pub mod build; pub mod build_env; pub mod utils; use crate::build::is_cross_compiling; use crate::build_env::sysroot_path; use crate::build_env::target_env; use crate::utils::path_between; use crate::utils::path_to_str; use std::env; use std::ffi::OsStr; use std::path::Path; use std::path::PathBuf; use std::process::Command; #[doc(hidden)] pub use anyhow::{Context, Result}; /// Decorator for the std::process::Command adding a some chainable helpers. /// /// Mostly useful for calling `./configure` scripts. pub trait CommandExt { /// Add this argument to the commands, but only on macos. fn arg_for_macos>(&mut self, arg: S) -> Result<&mut Command>; /// Add a `--prefix` to point to a toolchain sysroot or the /, depending on /// dinghy environment. fn configure_prefix>(&mut self, path: P) -> Result<&mut Command>; /// Adds pkgconfig environment variables to point to an eventual cross compiling sysroot. /// /// Usefull for compatibilty with pkg-config-rs up to 0.3.9 or to deal with /// `./configure` scripts. fn with_pkgconfig(&mut self) -> Result<&mut Command>; /// Propagate TARGET, TARGET_CC, TARGET_AR and TARGET_SYSROOT to a /// `./configure` script. fn with_toolchain(&mut self) -> Result<&mut Command>; } impl CommandExt for Command { fn arg_for_macos>(&mut self, arg: S) -> Result<&mut Command> { if env::var("TARGET") .map(|target| target.contains("-apple-darwin")) .unwrap_or(false) { self.arg(arg.as_ref()); } Ok(self) } fn configure_prefix>(&mut self, prefix_dir: P) -> Result<&mut Command> { self.args(&[ "--prefix", path_to_str(&path_between( sysroot_path().unwrap_or(PathBuf::from("/")), prefix_dir, ))?, ]); Ok(self) } fn with_pkgconfig(&mut self) -> Result<&mut Command> { if is_cross_compiling()? { if let Ok(value) = target_env("PKG_CONFIG_PATH") { log::info!("Running command with PKG_CONFIG_PATH:{:?}", value); self.env("PKG_CONFIG_PATH", value); } if let Ok(value) = target_env("PKG_CONFIG_LIBDIR") { log::info!("Running command with PKG_CONFIG_LIBDIR:{:?}", value); self.env("PKG_CONFIG_LIBDIR", value); } if let Ok(value) = target_env("PKG_CONFIG_SYSROOT_DIR") { log::info!("Running command with PKG_CONFIG_SYSROOT_DIR:{:?}", value); self.env("PKG_CONFIG_SYSROOT_DIR", value); } } Ok(self) } fn with_toolchain(&mut self) -> Result<&mut Command> { if is_cross_compiling()? { if let Ok(target) = env::var("TARGET") { self.arg(format!("--host={}", target)); } if let Ok(cc) = env::var("TARGET_CC") { self.arg(format!("CC={}", cc)); } if let Ok(ar) = env::var("TARGET_AR") { self.arg(format!("AR={}", ar)); } if let Ok(sysroot) = env::var("TARGET_SYSROOT") { self.arg(format!("--with-sysroot={}", &sysroot)); } } Ok(self) } } ================================================ FILE: dinghy-build/src/utils.rs ================================================ //! Some helpers around Path and PathBuf manipulations. use super::Result; use anyhow::anyhow; use std::path::Path; use std::path::PathBuf; /// Wraps the annoying PathBuf to string conversion in one single call. pub fn path_to_str(path: &PathBuf) -> Result<&str> { Ok(path .to_str() .ok_or(anyhow!("Not a valid UTF-8 path ({})", path.display()))?) } /// Finds the path to `to` relative from `from`. pub fn path_between, P2: AsRef>(from: P1, to: P2) -> PathBuf { let mut path = PathBuf::from("/"); for _ in from.as_ref() { path.push(".."); } for dir in to.as_ref().iter().skip(1) { path.push(dir); } path } ================================================ FILE: dinghy-lib/Cargo.toml ================================================ [package] name = "dinghy-lib" version = "0.8.5-pre" authors = ["Mathieu Poumeyrol "] license = "MIT/Apache-2.0" description = "Cross-compilation made easier - see main crate cargo-dinghy" homepage = "https://medium.com/snips-ai/dinghy-painless-rust-tests-and-benches-on-ios-and-android-c9f94f81d305#.c2sx7two8" repository = "https://github.com/sonos/dinghy" keywords = [ "tests", "mobile", "ios", "android", "cargo" ] build="build.rs" categories = [ "development-tools::cargo-plugins", "development-tools::testing" , "development-tools::profiling" ] readme = "../README.md" edition = "2018" [dependencies] anyhow = "1" dinghy-build = { path = "../dinghy-build" } dirs = "5" filetime = "0.2" log = "0.4" itertools = "0.10" plist = "1" regex = "1.0" json = "0.12" ignore = "0.4" serde = { version = "1.0", features = ["derive"] } toml = "0.7" shell-escape = "0.1" walkdir = "2.0" which = "=4.0" shellexpand="3" semver = "1" cargo_metadata.workspace=true cargo-platform.workspace=true colored = "2.0.0" lazy_static = "1.4.0" dyn-clone = "1.0.8" fs-err = "2.11.0" tempfile = "3.1" [dev-dependencies] tempfile = "3.1" ================================================ FILE: dinghy-lib/build.rs ================================================ fn main() { println!( "cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap() ); } ================================================ FILE: dinghy-lib/src/android/device.rs ================================================ use crate::device::make_remote_app; use crate::errors::*; use crate::platform::regular_platform::RegularPlatform; use crate::project::Project; use crate::utils::{get_current_verbosity, path_to_str, user_facing_log, LogCommandExt}; use crate::Build; use crate::BuildBundle; use crate::Device; use crate::DeviceCompatibility; use log::{debug, info, log_enabled}; use std::io::Write; use std::{fmt, io, path, process}; static ANDROID_WORK_DIR: &str = "/data/local/tmp/dinghy"; #[derive(Clone)] pub struct AndroidDevice { pub adb: path::PathBuf, pub id: String, pub supported_targets: Vec<&'static str>, } impl AndroidDevice { pub fn from_id(adb: path::PathBuf, id: &str) -> Result { for prop in &[ "ro.product.cpu.abilist", "ro.product.cpu.abi", "ro.product.cpu.abi2", ] { let getprop_output = process::Command::new(&adb) .args(&["-s", id, "shell", "getprop", prop]) .log_invocation(3) .output()?; let abilist = String::from_utf8(getprop_output.stdout)?; debug!( "Android device {}, getprop {} returned {}", id, prop, abilist.trim() ); if abilist.trim().len() > 0 { let supported_targets = abilist .trim() .split(",") .filter_map(|abi| { Some(match abi { "arm64-v8a" => "aarch64-linux-android", "armeabi-v7a" => "armv7-linux-androideabi", "armeabi" => "arm-linux-androideabi", "x86" => "i686-linux-android", "x86_64" => "x86_64-linux-android", _ => return None, }) }) .collect::>(); return Ok(AndroidDevice { adb, id: id.into(), supported_targets: supported_targets, }); } } bail!("Could not match a platform to the device") } fn adb(&self) -> Result { let mut command = process::Command::new(&self.adb); command.arg("-s").arg(&self.id); Ok(command) } fn install_app(&self, project: &Project, build: &Build) -> Result<(BuildBundle, BuildBundle)> { info!("Install {} to {}", build.runnable.id, self.id); user_facing_log( "Installing", &format!("{} to {}", build.runnable.id, self.id), 0, ); if !self .adb()? .arg("shell") .arg("mkdir") .arg("-p") .arg(ANDROID_WORK_DIR) .log_invocation(2) .status()? .success() { bail!( "Failure to create dinghy work dir '{:?}' on target android device", ANDROID_WORK_DIR ) } let build_bundle = make_remote_app(project, build)?; let remote_bundle = AndroidDevice::to_remote_bundle(&build_bundle)?; self.sync( &build_bundle.bundle_dir, &remote_bundle .bundle_dir .parent() .ok_or_else(|| anyhow!("Invalid path {}", remote_bundle.bundle_dir.display()))?, )?; self.sync( &build_bundle.lib_dir, &remote_bundle .lib_dir .parent() .ok_or_else(|| anyhow!("Invalid path {}", remote_bundle.lib_dir.display()))?, )?; debug!("Chmod target exe {}", remote_bundle.bundle_exe.display()); if !self .adb()? .arg("shell") .arg("chmod") .arg("755") .arg(&remote_bundle.bundle_exe) .log_invocation(2) .status()? .success() { bail!("Failure in android install"); } Ok((build_bundle, remote_bundle)) } fn sync, TP: AsRef>( &self, from_path: FP, to_path: TP, ) -> Result<()> { // Seems overkill... // let _ = self.adb()?.arg("shell").arg("rm").arg("-rf").arg(to_path.as_ref()).status()?; // Need parent as adb let mut command = self.adb()?; command .arg("push") .arg("--sync") .arg(from_path.as_ref()) .arg(to_path.as_ref()); if !log_enabled!(::log::Level::Debug) { command.stdout(::std::process::Stdio::null()); command.stderr(::std::process::Stdio::null()); } debug!("Running {:?}", command); if !command.log_invocation(2).status()?.success() { bail!("Error syncing android directory ({:?})", command) } else { Ok(()) } } fn to_remote_bundle(build_bundle: &BuildBundle) -> Result { build_bundle.replace_prefix_with(ANDROID_WORK_DIR) } } impl DeviceCompatibility for AndroidDevice { fn is_compatible_with_regular_platform(&self, platform: &RegularPlatform) -> bool { if platform.id.starts_with("auto-android") { let cpu = platform.id.split("-").nth(2).unwrap(); self.supported_targets .iter() .any(|target| target.starts_with(cpu)) } else { self.supported_targets .contains(&&*platform.toolchain.binutils_prefix) } } } impl Device for AndroidDevice { fn clean_app(&self, build_bundle: &BuildBundle) -> Result<()> { let remote_bundle = AndroidDevice::to_remote_bundle(build_bundle)?; debug!("Cleaup device"); if !self .adb()? .arg("shell") .arg("rm") .arg("-rf") .arg(&remote_bundle.bundle_dir) .log_invocation(1) .status()? .success() { bail!("Failure in android clean") } if !self .adb()? .arg("shell") .arg("rm") .arg("-rf") .arg(&remote_bundle.lib_dir) .log_invocation(1) .status()? .success() { bail!("Failure in android clean") } Ok(()) } fn debug_app( &self, _project: &Project, _build: &Build, _args: &[&str], _envs: &[&str], ) -> Result { unimplemented!() } fn id(&self) -> &str { &self.id } fn name(&self) -> &str { "android device" } fn run_app( &self, project: &Project, build: &Build, args: &[&str], envs: &[&str], ) -> Result { let args: Vec = args .iter() .map(|&a| ::shell_escape::escape(a.into()).to_string()) .collect(); let (build_bundle, remote_bundle) = self.install_app(&project, &build)?; let command = format!( "cd '{}'; RUST_BACKTRACE=1 {} DINGHY=1 LD_LIBRARY_PATH=\"{}:$LD_LIBRARY_PATH\" {} {} ; echo FORWARD_RESULT_TO_DINGHY_BECAUSE_ADB_DOES_NOT=$?", path_to_str(&remote_bundle.bundle_dir)?, envs.join(" "), path_to_str(&remote_bundle.lib_dir)?, path_to_str(&remote_bundle.bundle_exe)?, args.join(" ")); info!("Run {} on {}", build.runnable.id, self.id); if get_current_verbosity() < 1 { // we log the full command for verbosity > 1, just log a short message when the user // didn't ask for verbose output user_facing_log( "Running", &format!("{} on {}", build.runnable.id, self.id), 0, ); } if !self .adb()? .arg("shell") .arg(&command) .log_invocation(1) .output() .with_context(|| format!("Couldn't run {} using adb.", build.runnable.exe.display())) .and_then(|output| { if output.status.success() { let _ = io::stdout().write(output.stdout.as_slice()); let _ = io::stderr().write(output.stderr.as_slice()); String::from_utf8(output.stdout).with_context(|| { format!("Couldn't run {} using adb.", build.runnable.exe.display()) }) } else { bail!("Couldn't run {} using adb.", build.runnable.exe.display()) } }) .map(|output| output.lines().last().unwrap_or("").to_string()) .map(|last_line| { last_line.contains("FORWARD_RESULT_TO_DINGHY_BECAUSE_ADB_DOES_NOT=0") })? { bail!("Failed") } Ok(build_bundle) } } impl fmt::Display for AndroidDevice { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { write!(fmt, "Android/{}", self.id) } } impl fmt::Debug for AndroidDevice { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { Ok(fmt.write_str( format!( "Android {{ \"id\": \"{}\", \"supported_targets\": {:?} }}", self.id, self.supported_targets ) .as_str(), )?) } } ================================================ FILE: dinghy-lib/src/android/mod.rs ================================================ use crate::config::PlatformConfiguration; use crate::toolchain::ToolchainConfig; use crate::{Device, Platform, PlatformManager, Result}; use fs_err as fs; use std::fs::FileType; use std::{env, path, process}; pub use self::device::AndroidDevice; use crate::android::platform::AndroidPlatform; use crate::utils::LogCommandExt; use anyhow::{anyhow, bail, Context}; use log::debug; mod device; mod platform; pub struct AndroidManager { adb: path::PathBuf, } impl PlatformManager for AndroidManager { fn devices(&self) -> Result>> { let result = process::Command::new(&self.adb) .arg("devices") .log_invocation(3) .output()?; let mut devices = vec![]; let device_regex = ::regex::Regex::new(r#"^(\S+)\tdevice\r?$"#)?; for line in String::from_utf8(result.stdout)?.split("\n").skip(1) { if let Some(caps) = device_regex.captures(line) { let d = AndroidDevice::from_id(self.adb.clone(), &caps[1])?; debug!( "Discovered Android device {} ({:?})", d, d.supported_targets ); devices.push(Box::new(d) as Box); } } Ok(devices) } fn platforms(&self) -> Result>> { if let Some(ndk) = ndk()? { let default_api_level = "21"; debug!("Android NDK: {:?}", ndk); let version = ndk_version(&ndk)?; let major = version .split(".") .next() .ok_or_else(|| anyhow!("Invalid version found for ndk {:?}", &ndk))?; let major: usize = major .parse() .with_context(|| format!("Invalid version found for ndk {:?}", &ndk))?; debug!( "Android ndk: {:?}, ndk version: {}, major: {}", ndk, version, major ); if major >= 19 { let mut platforms = vec![]; let prebuilt = ndk.join("toolchains/llvm/prebuilt"); let tools = prebuilt .read_dir()? .filter(|it| { // ensure we only check dirs as macOS may add pesky .DS_Store files... it.as_ref() .ok() .and_then(|it| it.file_type().ok().as_ref().map(FileType::is_dir)) .unwrap_or(false) }) .next() .ok_or_else(|| anyhow!("No tools in toolchain"))??; let bin = tools.path().join("bin"); debug!("Android tools bin: {:?}", bin); let libclang_path = tools.path().join(if major >= 26 { "lib" } else { "lib64" }); for (rustc_cpu, cc_cpu, binutils_cpu, abi_kind) in &[ ("aarch64", "aarch64", "aarch64", "android"), ("armv7", "armv7a", "arm", "androideabi"), ("i686", "i686", "i686", "android"), ("x86_64", "x86_64", "x86_64", "android"), ] { let mut api_levels: Vec = Vec::new(); for entry in fs::read_dir(tools.path().join(format!( "sysroot/usr/lib/{}-linux-{}", binutils_cpu, abi_kind )))? { let entry = entry?; if entry.file_type()?.is_dir() { let folder_name = entry.file_name().into_string().unwrap(); match folder_name.parse::() { Ok(_) => api_levels.push(folder_name), Err(_) => {} } } } api_levels.sort(); let create_platform = |api: &str, suffix: &str| { let id = format!("auto-android-{}{}", rustc_cpu, suffix); let tc = ToolchainConfig { bin_dir: bin.clone(), rustc_triple: format!("{}-linux-{}", rustc_cpu, abi_kind), root: prebuilt.clone(), sysroot: Some(tools.path().join("sysroot")), cc: "clang".to_string(), cxx: "clang++".to_string(), binutils_prefix: format!("{}-linux-{}", binutils_cpu, abi_kind), cc_prefix: format!("{}-linux-{}{}", cc_cpu, abi_kind, api), }; AndroidPlatform::new( PlatformConfiguration::default(), id, tc, major, ndk.clone(), libclang_path.clone(), ) }; for api in api_levels.iter() { platforms.push(create_platform(&api, &format!("-api{}", api))?); } if !api_levels.is_empty() { platforms.push(create_platform( api_levels .first() .expect("The api level vector shouldn't be empty"), "-min", )?); platforms.push(create_platform( api_levels .last() .expect("The api level vector shouldn't be empty"), "-latest", )?); } platforms.push(create_platform(default_api_level, "")?); } return Ok(platforms); } } return Ok(vec![]); } } impl AndroidManager { pub fn probe() -> Option { match adb() { Ok(adb) => { debug!("ADB found: {:?}", adb); Some(AndroidManager { adb }) } Err(_) => { debug!("adb not found in path, android disabled"); None } } } } fn probable_sdk_locs() -> Result> { let mut v = vec![]; for var in &[ "ANDROID_HOME", "ANDROID_SDK", "ANDROID_SDK_ROOT", "ANDROID_SDK_HOME", ] { if let Ok(path) = env::var(var) { let path = path::Path::new(&path); if path.is_dir() { v.push(path.to_path_buf()) } } } if let Ok(home) = env::var("HOME") { let mac = path::Path::new(&home).join("Library/Android/sdk"); if mac.is_dir() { v.push(mac); } let linux = path::Path::new(&home).join("Android/Sdk"); if linux.is_dir() { v.push(linux); } } let casks = path::PathBuf::from("/usr/local/Caskroom/android-sdk"); if casks.is_dir() { for kid in casks.read_dir()? { let kid = kid?; if kid.file_name() != ".metadata" { v.push(kid.path()); } } } debug!("Candidates SDK: {:?}", v); Ok(v) } fn ndk() -> Result> { if let Ok(path) = env::var("ANDROID_NDK_HOME") { return Ok(Some(path.into())); } if let Ok(path) = env::var("ANDROID_NDK") { return Ok(Some(path.into())); } for sdk in probable_sdk_locs()? { if sdk.join("ndk-bundle/source.properties").is_file() { return Ok(Some(sdk.join("ndk-bundle"))); } if let Some(ndk) = find_non_legacy_ndk(&sdk)? { return Ok(Some(ndk)); } } debug!("Android NDK not found"); Ok(None) } fn ndk_version(ndk: &path::Path) -> Result { let sources_prop_file = ndk.join("source.properties"); let props = fs::read_to_string(&sources_prop_file) .with_context(|| format!("Reading prop file {sources_prop_file:?}"))?; let revision_line = props .split("\n") .find(|l| l.starts_with("Pkg.Revision")) .with_context(|| { format!( "{:?} does not contain a Pkg.Revision line. Invalid SDK?", sources_prop_file ) })?; Ok(revision_line.split(" ").last().unwrap().to_string()) } fn adb() -> Result { fn try_out(command: &path::Path) -> bool { match process::Command::new(command) .arg("--version") .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .log_invocation(3) .status() { Ok(_) => true, Err(_) => false, } } if let Ok(adb) = env::var("DINGHY_ANDROID_ADB") { return Ok(adb.into()); } if let Ok(adb) = ::which::which("adb") { return Ok(adb); } for loc in probable_sdk_locs()? { let adb = loc.join("platform-tools/adb"); if try_out(&adb) { return Ok(adb.into()); } } bail!("Adb could be found") } fn find_non_legacy_ndk(sdk: &path::Path) -> Result> { let ndk_root = sdk.join("ndk"); if !ndk_root.is_dir() { return Ok(None); } let ndk = fs::read_dir(ndk_root)? .filter_map(Result::ok) .filter_map(|directory| { directory .path() .file_name() .and_then(|name| { let name = name.to_string_lossy(); // Filter out directory if we fail to parse directory name to semver semver::Version::parse(&name).ok() }) .map(|version| (directory, version)) }) .max_by(|left, right| { let left_version: &semver::Version = &left.1; let right_version: &semver::Version = &right.1; left_version.cmp(right_version) }) .map(|tuple| tuple.0.path()); Ok(ndk) } #[cfg(test)] mod tests { use super::*; #[test] fn test_find_non_legacy_ndk() { let sdk_dir = tempfile::tempdir().unwrap(); let sdk = sdk_dir.path(); let ndk_root = sdk.join("ndk"); let ndk_versions = ["21.1.123456", "21.3.6528147", "20.1.5948944"]; for version in &ndk_versions { let path = ndk_root.join(version); fs::create_dir_all(path).unwrap(); } let ndk = find_non_legacy_ndk(sdk).unwrap(); let expected = ndk_root.join("21.3.6528147"); assert_eq!(Some(expected), ndk); } #[test] fn test_find_non_legacy_ndk_on_non_existing_directory() { let sdk = tempfile::tempdir().unwrap(); let ndk = find_non_legacy_ndk(sdk.path()).unwrap(); assert_eq!(None, ndk); } } ================================================ FILE: dinghy-lib/src/android/platform.rs ================================================ use std::fmt::Formatter; use crate::platform::regular_platform::RegularPlatform; use crate::toolchain::ToolchainConfig; use crate::{platform, Result}; use crate::{Build, Device, Platform, PlatformConfiguration, Project, SetupArgs}; use dinghy_build::build_env::set_env; use std::io::Write; use std::path::PathBuf; use std::process::Command; pub struct AndroidPlatform { regular_platform: Box, toolchain_config: ToolchainConfig, ndk_major_version: usize, ndk_path: PathBuf, libclang_path: PathBuf, } impl std::fmt::Debug for AndroidPlatform { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.id()) } } impl AndroidPlatform { pub fn new( configuration: PlatformConfiguration, id: String, toolchain_config: ToolchainConfig, ndk_major_version: usize, ndk_path: PathBuf, libclang_path: PathBuf, ) -> Result> { Ok(Box::new(Self { regular_platform: RegularPlatform::new_with_tc( configuration, id, toolchain_config.clone(), )?, toolchain_config, ndk_major_version, ndk_path, libclang_path, })) } } impl Platform for AndroidPlatform { fn setup_env(&self, project: &Project, setup_args: &SetupArgs) -> anyhow::Result<()> { self.regular_platform.setup_env(project, setup_args)?; if self.ndk_major_version >= 23 { log::trace!("Setup linker with android ndk23+ hack..."); let hack_dir = project .metadata .target_directory .join(self.rustc_triple()) .join(self.id()) .join("ndk23-hack"); std::fs::create_dir_all(&hack_dir)?; let mut hack_file = std::fs::File::create(hack_dir.join("libgcc.a"))?; hack_file.write_all("INPUT(-lunwind)".as_bytes())?; let mut linker_cmd = self.toolchain_config.generate_linker_command(&setup_args); linker_cmd.push_str(" -L"); linker_cmd.push_str(hack_dir.canonicalize()?.to_str().unwrap()); self.toolchain_config.setup_linker( &self.id(), &linker_cmd, &project.metadata.workspace_root, )?; self.toolchain_config .setup_tool("AR", &self.toolchain_config.naked_executable("llvm-ar"))?; } if self.ndk_major_version >= 17 { // bindgen need this to use the proper imports set_env("DINGHY_BUILD_LIBCLANG_PATH", &self.libclang_path) } if std::env::var("ANDROID_NDK").is_err() { set_env("ANDROID_NDK", self.ndk_path.canonicalize()?) } if std::env::var("ANDROID_NDK_HOME").is_err() { set_env("ANDROID_NDK_HOME", self.ndk_path.canonicalize()?) } Ok(()) } fn id(&self) -> String { self.regular_platform.id() } fn is_compatible_with(&self, device: &dyn Device) -> bool { self.regular_platform.is_compatible_with(device) } fn is_host(&self) -> bool { self.regular_platform.is_host() } fn rustc_triple(&self) -> &str { self.regular_platform.rustc_triple() } fn strip(&self, build: &mut Build) -> anyhow::Result<()> { if self.ndk_major_version >= 23 { build.runnable = platform::strip_runnable( &build.runnable, Command::new(self.toolchain_config.naked_executable("llvm-strip")), )?; Ok(()) } else { self.regular_platform.strip(build) } } fn sysroot(&self) -> anyhow::Result> { self.regular_platform.sysroot() } } ================================================ FILE: dinghy-lib/src/apple/device.rs ================================================ use super::{xcode, AppleSimulatorType}; use crate::apple::AppleDevicePlatform; use crate::device::make_remote_app_with_name; use crate::errors::*; use crate::project::Project; use crate::utils::LogCommandExt; use crate::utils::{get_current_verbosity, user_facing_log}; use crate::Build; use crate::BuildBundle; use crate::Device; use crate::DeviceCompatibility; use crate::Runnable; use colored::Colorize; use fs_err as fs; use itertools::Itertools; use log::debug; use std::fmt; use std::fmt::Display; use std::fmt::Formatter; use std::io::{BufRead, BufReader}; use std::path::Path; use std::process::{self, Stdio}; use std::time::Duration; #[derive(Clone, Debug)] pub struct IosDevice { pub id: String, pub name: String, pub arch_cpu: &'static str, rustc_triple: String, pub os: String, } #[derive(Clone, Debug)] pub struct AppleSimDevice { pub id: String, pub name: String, pub os: String, pub sim_type: AppleSimulatorType, } unsafe impl Send for IosDevice {} impl IosDevice { pub fn new(name: String, id: String, arch_cpu: &str, os: String) -> Result { let cpu = match &*arch_cpu { "arm64" | "arm64e" => "aarch64", _ => "armv7", }; Ok(IosDevice { name, id, os, arch_cpu: cpu.into(), rustc_triple: format!("{}-apple-ios", cpu), }) } fn is_pre_ios_17(&self) -> Result { Ok(semver::VersionReq::parse(&self.os)? .comparators .get(0) .ok_or_else(|| anyhow!("Invalid iOS version: {}", self.os))? .major < 17) } fn is_locked(&self) -> Result { let result = process::Command::new("xcrun") .args( "devicectl device info lockState --quiet --json-output /dev/stdout --device" .split_whitespace(), ) .arg(&self.id) .log_invocation(1) .output() .context("Failed to run devicectl device info lockState")?; if !result.status.success() { bail!("Device lock query failed\n",) } Ok( json::parse(std::str::from_utf8(&result.stdout)?)?["result"]["passcodeRequired"] .as_bool() .unwrap(), ) } fn make_app( &self, project: &Project, build: &Build, runnable: &Runnable, ) -> Result { let signing = xcode::look_for_signature_settings(&self.id)? .pop() .ok_or_else(|| anyhow!("no signing identity found"))?; let app_id = signing .name .split(" ") .last() .ok_or_else(|| anyhow!("no app id ?"))?; let mut build_bundle = make_apple_app(project, build, runnable, &app_id, None)?; build_bundle.app_id = Some(app_id.to_owned()); super::xcode::sign_app(&build_bundle, &signing)?; Ok(build_bundle) } fn install_app( &self, project: &Project, build: &Build, runnable: &Runnable, ) -> Result { user_facing_log( "Installing", &format!("{} to {} ({})", build.runnable.id, self.id, self.name), 0, ); let build_bundle = self.make_app(project, build, runnable)?; let bundle = build_bundle.bundle_dir.to_string_lossy(); if self.is_pre_ios_17()? { self.install_app_with_ios_deploy(&bundle)?; return Ok(build_bundle); } // xcrun devicectl device install app --device 00008110-001XXXXXXXXXX ./xgen/Build/Products/Release-iphoneos/nilo.app let result = process::Command::new("xcrun") .args("devicectl device install app --device".split_whitespace()) .arg(&self.id) .arg(&*bundle) .log_invocation(1) .status() .context("Failed to run devicectl device install app")?; if !result.success() { bail!("Installation on device failed\n",) } Ok(build_bundle) } fn run_remote( &self, build_bundle: &BuildBundle, args: &[&str], envs: &[&str], debugger: bool, ) -> Result<()> { if self.is_pre_ios_17()? { return self.run_remote_with_ios_deploy(build_bundle, args, envs, debugger); } let app_list = process::Command::new("pymobiledevice3") .args("apps list --no-color --udid".split_whitespace()) .arg(&self.id) .output()?; let app_list = json::parse(std::str::from_utf8(&app_list.stdout)?).with_context(|| { format!( "Ran `pymobiledevice3 apps list --no-color --udid {}`, could not parse expected JSON output.", self.id, ) })?; let app_path = build_bundle.bundle_dir.to_string_lossy(); let app = app_list .entries() .find(|e| e.0 == build_bundle.app_id.as_ref().unwrap()) .unwrap() .1; let remote_path = app["Path"].to_string(); let tunnel = process::Command::new("sudo") .arg("-p") .arg(format!( "Please enter %p's password on %h to start a tunnel to '{}' (sudo):", self.name )) .args("pymobiledevice3 remote start-tunnel --script-mode --udid".split_whitespace()) .arg(&self.id) .stderr(Stdio::inherit()) .stdin(Stdio::inherit()) .stdout(Stdio::piped()) .spawn()?; let mut rsd = String::new(); BufReader::new(tunnel.stdout.unwrap()).read_line(&mut rsd)?; debug!("iOS RSD tunnel started: {rsd}"); // start the debugserver let server = process::Command::new("pymobiledevice3") .args("developer debugserver start-server --rsd".split_whitespace()) .args(rsd.trim().split_whitespace()) .stderr(Stdio::inherit()) .stdout(Stdio::piped()) .spawn()?; let lldb_connection_string = BufReader::new(server.stdout.unwrap()) .lines() .find(|l| l.as_ref().unwrap().contains("process connect connect://")) .unwrap() .unwrap(); let connection_details = lldb_connection_string.split_whitespace().nth(3).unwrap(); debug!("iOS debugserver started: {connection_details}"); if self.is_locked()? { eprint!( "{}", format!("\n\n Please unlock {}! ", &self.name).bright_yellow() ); loop { std::thread::sleep(Duration::from_millis(300)); if !self.is_locked()? { eprintln!("{}", " All good, yay!\n".bright_green()); break; } } } let tempdir = tempfile::TempDir::with_prefix("dinghy-lldb")?; let script_path = tempdir.path().join("run.lldb"); // see https://stackoverflow.com/questions/77865860/lldb-hangs-when-trying-to-execute-command-with-o // for the terrible async thing std::fs::write( &script_path, format!( " platform select remote-ios target create {app_path} script lldb.target.module[0].SetPlatformFileSpec(lldb.SBFileSpec('{remote_path}')) script old_debug = lldb.debugger.GetAsync() script lldb.debugger.SetAsync(True) process connect {connection_details} script lldb.debugger.SetAsync(old_debug) run {} exit ", args.iter() .map(|&s| shell_escape::escape(s.into())) .join(" ") ), )?; let lldb = process::Command::new("lldb") .arg("--batch") .arg("-s") .arg(script_path) .stderr(Stdio::inherit()) .stdout(Stdio::piped()) .spawn()?; let mut lines = BufReader::new(lldb.stdout.unwrap()).lines(); while !lines.next().unwrap()?.starts_with("(lldb) run") {} for line in lines { let line = line?; println!("{}", line); if line.contains("exited with status = ") { let rv = line.split_whitespace().nth(6).unwrap(); println!("returns: {rv}"); if rv == "0" { return Ok(()); } else { bail!("Failed") } } } Ok(()) } // LEGACY IOS-DEPLOY BASED WORKFLOW (iOS<17) fn install_app_with_ios_deploy(&self, bundle: &str) -> Result<()> { process::Command::new("ios-deploy") .args(&["-i", &self.id, "-b", &bundle, "-n"]) .log_invocation(1) .output() .context("Failed to run ios-deploy")? .status; Ok(()) } fn run_remote_with_ios_deploy( &self, build_bundle: &BuildBundle, args: &[&str], envs: &[&str], debugger: bool, ) -> Result<()> { let bundle = build_bundle.bundle_dir.to_string_lossy(); let mut command = process::Command::new("ios-deploy"); command.args(&["-i", &self.id, "-b", &bundle, "-m"]); command.args(&["-a", &args.join(" ")]); command.args(&["-s", &envs.join(" ")]); command.arg(if debugger { "-d" } else { "-I" }); command.stderr(process::Stdio::inherit()); command.stdout(process::Stdio::inherit()); let status = command .log_invocation(1) .output() .context("Failed to run ios-deploy")? .status; if !status.success() { bail!("Run on device failed") } Ok(()) } } impl Device for IosDevice { fn clean_app(&self, _build_bundle: &BuildBundle) -> Result<()> { unimplemented!() } fn debug_app( &self, project: &Project, build: &Build, args: &[&str], envs: &[&str], ) -> Result { let build_bundle = self.install_app(project, build, &build.runnable)?; if get_current_verbosity() < 1 { // we log the full command for verbosity > 1, just log a short message when the user // didn't ask for verbose output user_facing_log( "Debugging", &format!("{} on {}", build.runnable.id, self.id), 0, ); } self.run_remote(&build_bundle, args, envs, true)?; Ok(build_bundle) } fn id(&self) -> &str { &self.id } fn name(&self) -> &str { &self.name } fn run_app( &self, project: &Project, build: &Build, args: &[&str], envs: &[&str], ) -> Result { let build_bundle = self.install_app(project, build, &build.runnable)?; if get_current_verbosity() < 1 { // we log the full command for verbosity > 1, just log a short message when the user // didn't ask for verbose output user_facing_log( "Running", &format!("{} on {}", build.runnable.id, self.id), 0, ); } self.run_remote(&build_bundle, args, envs, false)?; Ok(build_bundle) } } impl AppleSimDevice { fn install_app( &self, project: &Project, build: &Build, runnable: &Runnable, ) -> Result { user_facing_log( "Installing", &format!("{} to {}", build.runnable.id, self.id), 0, ); let build_bundle = self.make_app(project, build, runnable)?; let _ = process::Command::new("xcrun") .args(&["simctl", "uninstall", &self.id, "Dinghy"]) .log_invocation(2) .status()?; let stat = process::Command::new("xcrun") .args(&[ "simctl", "install", &self.id, build_bundle .bundle_dir .to_str() .ok_or_else(|| anyhow!("conversion to string"))?, ]) .log_invocation(1) .status()?; if stat.success() { Ok(build_bundle) } else { bail!( "Failed to install {} for {}", runnable.exe.display(), self.id ) } } fn make_app( &self, project: &Project, build: &Build, runnable: &Runnable, ) -> Result { make_apple_app(project, build, runnable, "Dinghy", Some(&self.sim_type)) } } impl Device for AppleSimDevice { fn clean_app(&self, _build_bundle: &BuildBundle) -> Result<()> { unimplemented!() } fn debug_app( &self, project: &Project, build: &Build, args: &[&str], envs: &[&str], ) -> Result { let runnable = &build.runnable; let build_bundle = self.install_app(project, build, runnable)?; let install_path = String::from_utf8( process::Command::new("xcrun") .args(&["simctl", "get_app_container", &self.id, "Dinghy"]) .log_invocation(2) .output()? .stdout, )?; if get_current_verbosity() < 1 { // we log the full command for verbosity > 1, just log a short message when the user // didn't ask for verbose output user_facing_log( "Debugging", &format!("{} on {}", build.runnable.id, self.id), 0, ); } launch_lldb_simulator(&self, &install_path, args, envs, true)?; Ok(build_bundle) } fn id(&self) -> &str { &self.id } fn name(&self) -> &str { &self.name } fn run_app( &self, project: &Project, build: &Build, args: &[&str], envs: &[&str], ) -> Result { let build_bundle = self.install_app(&project, &build, &build.runnable)?; if get_current_verbosity() < 1 { // we log the full command for verbosity > 1, just log a short message when the user // didn't ask for verbose output user_facing_log( "Running", &format!("{} on {}", build.runnable.id, self.id), 0, ); } launch_app(&self, args, envs)?; Ok(build_bundle) } } impl Display for IosDevice { fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { write!( fmt, "{} ({} {} {})", self.name, self.id, self.arch_cpu, self.os ) } } impl Display for AppleSimDevice { fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { write!(fmt, "{} ({} sim {})", self.name, self.id, self.os) } } impl DeviceCompatibility for IosDevice { fn is_compatible_with_simulator_platform(&self, platform: &AppleDevicePlatform) -> bool { if platform.sim.is_some() { return false; } if platform.toolchain.rustc_triple == self.rustc_triple.as_str() { return true; } return false; } } impl DeviceCompatibility for AppleSimDevice { fn is_compatible_with_simulator_platform(&self, platform: &AppleDevicePlatform) -> bool { if let Some(sim) = &platform.sim { self.sim_type == *sim } else { false } } } fn make_apple_app( project: &Project, build: &Build, runnable: &Runnable, app_id: &str, sim_type: Option<&AppleSimulatorType>, ) -> Result { use crate::project; let build_bundle = make_remote_app_with_name(project, build, Some("Dinghy.app"))?; project::rec_copy(&runnable.exe, build_bundle.bundle_dir.join("Dinghy"), false)?; let magic = process::Command::new("file") .arg( runnable .exe .to_str() .ok_or_else(|| anyhow!("path conversion to string: {:?}", runnable.exe))?, ) .log_invocation(3) .output()?; let magic = String::from_utf8(magic.stdout)?; let target = magic .split(" ") .last() .ok_or_else(|| anyhow!("empty magic"))?; xcode::add_plist_to_app(&build_bundle, target, app_id, sim_type)?; Ok(build_bundle) } fn launch_app(dev: &AppleSimDevice, app_args: &[&str], _envs: &[&str]) -> Result<()> { use std::io::Write; let dir = tempfile::TempDir::with_prefix("mobiledevice-rs-lldb")?; let tmppath = dir.path(); let mut install_path = String::from_utf8( process::Command::new("xcrun") .args(&["simctl", "get_app_container", &dev.id, "Dinghy"]) .log_invocation(2) .output()? .stdout, )?; install_path.pop(); let stdout = Path::new(&install_path) .join("stdout") .to_string_lossy() .into_owned(); let stdout_param = &format!("--stdout={}", stdout); let mut xcrun_args: Vec<&str> = vec![ "simctl", "launch", "--wait-for-debugger", stdout_param, &dev.id, "Dinghy", ]; xcrun_args.extend(app_args); debug!("Launching app via xcrun using args: {:?}", xcrun_args); let launch_output = process::Command::new("xcrun") .args(&xcrun_args) .log_invocation(1) .output()?; let launch_output = String::from_utf8_lossy(&launch_output.stdout); debug!("xcrun simctl launch output: {:?}", launch_output); // Output from the launch command should be "Dinghy: $PID" which is after the 8th character. let dinghy_pid = launch_output.split_at(8).1; // Attaching to the processes needs to be done in a script, not a commandline parameter or // lldb will say "no simulators found". let lldb_script_filename = tmppath.join("lldb-script"); let mut script = fs::File::create(&lldb_script_filename)?; write!(script, "attach {}\n", dinghy_pid)?; write!(script, "continue\n")?; write!(script, "quit\n")?; let output = process::Command::new("lldb") .arg("") .arg("-s") .arg(lldb_script_filename) .output()?; let test_contents = std::fs::read_to_string(&stdout) .with_context(|| format!("Reading llvm stdout from {stdout}"))?; println!("{}", test_contents); let output: String = String::from_utf8_lossy(&output.stdout).to_string(); debug!("lldb script: \n{}", output); // The stdout from lldb is something like: // // (lldb) attach 34163 // Process 34163 stopped // * thread #1, stop reason = signal SIGSTOP // frame #0: 0x00000001019cd000 dyld`_dyld_start // dyld`_dyld_start: // -> 0x1019cd000 <+0>: popq %rdi // 0x1019cd001 <+1>: pushq $0x0 // 0x1019cd003 <+3>: movq %rsp, %rbp // 0x1019cd006 <+6>: andq $-0x10, %rsp // Target 0: (Dinghy) stopped. // Executable module set to ..... // Architecture set to: x86_64h-apple-ios-. // (lldb) continue // Process 34163 resuming // Process 34163 exited with status = 101 (0x00000065) // (lldb) quit // // We need the "exit with status" line which is the 3rd from the last let exit_status_line = output .lines() .rev() .find(|line| line.contains("exited with status")); if let Some(exit_status_line) = exit_status_line { let words: Vec<&str> = exit_status_line.split_whitespace().rev().collect(); if let Some(exit_status) = words.get(1) { let exit_status = exit_status.parse::()?; if exit_status == 0 { Ok(()) } else { bail!("Test failure, exit code: {}", exit_status) } } else { panic!( "Failed to parse lldb exit line for an exit status. {:?}", words ); } } else { panic!("Failed to get the exit status line from lldb: {}", output); } } fn launch_lldb_simulator( dev: &AppleSimDevice, installed: &str, args: &[&str], envs: &[&str], debugger: bool, ) -> Result<()> { use std::io::Write; use std::process::Command; let dir = tempfile::TempDir::with_prefix("mobiledevice-rs-lldb")?; let tmppath = dir.path(); let lldb_script_filename = tmppath.join("lldb-script"); { let python_lldb_support = tmppath.join("helpers.py"); let helper_py = include_str!("helpers.py"); let helper_py = helper_py.replace("ENV_VAR_PLACEHOLDER", &envs.join("\", \"")); fs::File::create(&python_lldb_support)?.write_fmt(format_args!("{}", &helper_py))?; let mut script = fs::File::create(&lldb_script_filename)?; writeln!(script, "platform select ios-simulator")?; writeln!(script, "target create {}", installed)?; writeln!(script, "script pass")?; writeln!(script, "command script import {:?}", python_lldb_support)?; writeln!( script, "command script add -s synchronous -f helpers.start start" )?; writeln!( script, "command script add -f helpers.connect_command connect" )?; writeln!(script, "connect connect://{}", dev.id)?; if !debugger { writeln!(script, "start {}", args.join(" "))?; writeln!(script, "quit")?; } } let stat = Command::new("xcrun") .arg("lldb") .arg("-Q") .arg("-s") .arg(lldb_script_filename) .log_invocation(1) .status()?; if stat.success() { Ok(()) } else { bail!("LLDB returned error code {:?}", stat.code()) } } ================================================ FILE: dinghy-lib/src/apple/helpers.py ================================================ import os import lldb import shlex def connect_command(debugger, command, result, internal_dict): connect_url = command error = lldb.SBError() process = lldb.target.ConnectRemote(lldb.target.GetDebugger().GetListener(), connect_url, None, error) def set_remote_path(debugger, command, result, internal_dict): device_app = command error = lldb.SBError() lldb.target.modules[0].SetPlatformFileSpec(lldb.SBFileSpec(device_app)) def start(debugger, command, result, internal_dict): error = lldb.SBError() info = lldb.SBLaunchInfo(shlex.split(command)) info.SetEnvironmentEntries(["ENV_VAR_PLACEHOLDER"], True) proc = lldb.target.Launch(info, error) lockedstr = ': Locked' if proc.GetState() != lldb.eStateExited: print("process left in lldb state: %s"%(debugger.StateAsCString(proc.GetState()))) if lockedstr in str(error): print('\nDevice Locked\n') os._exit(254) elif proc.GetState() == lldb.eStateStopped: thread = proc.GetSelectedThread() print(thread) for frame in thread: print(" %s"%(frame)) os._exit(-1) elif not error.Success(): print(str(error)) if proc.exit_state != 0: os._exit(proc.exit_state) ================================================ FILE: dinghy-lib/src/apple/mod.rs ================================================ use std::collections::HashMap; use std::fmt::Display; pub use self::device::{AppleSimDevice, IosDevice}; pub use self::platform::AppleDevicePlatform; use crate::{Device, Platform, PlatformManager, Result}; use itertools::Itertools; mod device; mod platform; mod xcode; use anyhow::{anyhow, bail, Context}; use log::info; #[derive(Debug, Clone)] pub struct SignatureSettings { pub identity: SigningIdentity, pub file: String, pub entitlements: String, pub name: String, #[allow(dead_code)] pub profile: String, } #[derive(Debug, Clone)] pub struct SigningIdentity { #[allow(dead_code)] pub id: String, pub name: String, pub team: String, } #[derive(Debug, Clone, PartialEq)] pub enum AppleSimulatorType { Ios, Watchos, Tvos, } impl Display for AppleSimulatorType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let val = match self { AppleSimulatorType::Ios => "ios", AppleSimulatorType::Watchos => "watchos", AppleSimulatorType::Tvos => "tvos", }; f.write_str(val) } } pub struct IosManager { devices: Vec>, } impl IosManager { pub fn new() -> Result> { let devices = devices() .context("Could not list iOS devices")? .into_iter() .chain( simulators(AppleSimulatorType::Ios) .context("Could not list iOS simulators")? .into_iter(), ) .collect(); Ok(Some(IosManager { devices })) } } impl PlatformManager for IosManager { fn devices(&self) -> Result>> { Ok(self.devices.clone()) } fn platforms(&self) -> Result>> { [ "armv7", "armv7s", "aarch64", "i386", "x86_64", "aarch64-sim", ] .iter() .map(|arch| { let id = format!("auto-ios-{}", arch); let rustc_triple = if *arch != "aarch64-sim" { format!("{}-apple-ios", arch) } else { format!("aarch64-apple-ios-sim") }; let simulator = if *arch == "x86_64" || *arch == "aarch64-sim" { Some(AppleSimulatorType::Ios) } else { None }; AppleDevicePlatform::new( id, &rustc_triple, simulator, crate::config::PlatformConfiguration::default(), ) .map(|pf| pf as Box) }) .collect() } } pub struct WatchosManager { devices: Vec>, } impl WatchosManager { pub fn new() -> Result> { let devices = simulators(AppleSimulatorType::Watchos)?; Ok(Some(Self { devices })) } } impl PlatformManager for WatchosManager { fn devices(&self) -> Result>> { Ok(self.devices.clone()) } fn platforms(&self) -> Result>> { ["arm64_32", "aarch64", "x86_64-sim", "aarch64-sim"] .iter() .map(|arch| { let id = format!("auto-watchos-{}", arch); // Apple watch simulator targets are x86_64-apple-watchos-sim or // aarch64-apple-watchos-sim let rustc_triple = if *arch == "aarch64-sim" { format!("aarch64-apple-watchos-sim") } else if *arch == "x86_64-sim" { format!("x86_64-apple-watchos-sim") } else { format!("{}-apple-watchos", arch) }; let simulator = if *arch == "x86_64-sim" || *arch == "aarch64-sim" { Some(AppleSimulatorType::Watchos) } else { None }; AppleDevicePlatform::new( id, &rustc_triple, simulator, crate::config::PlatformConfiguration::default(), ) .map(|pf| pf as Box) }) .collect() } } pub struct TvosManager { devices: Vec>, } impl TvosManager { pub fn new() -> Result> { let devices = simulators(AppleSimulatorType::Tvos)?; Ok(Some(Self { devices })) } } impl PlatformManager for TvosManager { fn devices(&self) -> Result>> { Ok(self.devices.clone()) } fn platforms(&self) -> Result>> { ["aarch64", "x86_64", "aarch64-sim"] .iter() .map(|arch| { let id = format!("auto-tvos-{}", arch); let rustc_triple = if *arch != "aarch64-sim" { format!("{}-apple-tvos", arch) } else { format!("aarch64-apple-tvos-sim") }; let simulator = if *arch == "x86_64" || *arch == "aarch64-sim" { Some(AppleSimulatorType::Tvos) } else { None }; AppleDevicePlatform::new( id, &rustc_triple, simulator, crate::config::PlatformConfiguration::default(), ) .map(|pf| pf as Box) }) .collect() } } fn simulators(sim_type: AppleSimulatorType) -> Result>> { let sims_list = ::std::process::Command::new("xcrun") .args(&[ "simctl", "list", "--json", "devices", sim_type.to_string().as_str(), ]) .output()?; if !sims_list.status.success() { info!( "Failed while looking for ios simulators. It this is not expected, you need to make sure `xcrun simctl list --json` works." ); return Ok(vec![]); } let sims_list = String::from_utf8(sims_list.stdout)?; let sims_list = json::parse(&sims_list) .with_context(|| "Could not parse output for: `xcrun simctl list --json devices` as json. Please try to make this command work and retry.")?; let mut sims: Vec> = vec![]; for (ref k, ref v) in sims_list["devices"].entries() { for ref sim in v.members() { if sim["state"] == "Booted" { sims.push(Box::new(AppleSimDevice { name: sim["name"] .as_str() .ok_or_else(|| anyhow!("unexpected simulator list format (missing name)"))? .to_string(), id: sim["udid"] .as_str() .ok_or_else(|| anyhow!("unexpected simulator list format (missing udid)"))? .to_string(), os: k.split(" ").last().unwrap().to_string(), sim_type: sim_type.clone(), })) } } } Ok(sims) } fn devices() -> Result>> { let mut devices: HashMap = Default::default(); if which::which("xcrun").is_err() { log::warn!("xcrun not found. Apple devices support disabled. Consider installing XCode and its command line tools."); return Ok(vec![]); } if !std::process::Command::new("xcrun") .arg("--find") .arg("devicectl") .output()? .status .success() { log::warn!("xcrun devicectl not found. Apple devices support disabled. Consider updating XCode and its command line tools."); return Ok(vec![]); } devices_from_devicectl(&mut devices)?; devices_from_ios_deploy(&mut devices)?; Ok(devices .into_values() .map(|d| Box::new(d) as _) .collect_vec()) } fn devices_from_devicectl(devices: &mut HashMap) -> Result<()> { let tempdir = tempfile::TempDir::with_prefix("dinghy-ios")?; let tmpjson = tempdir.path().join("json"); let devicectl = std::process::Command::new("xcrun") .args("devicectl list devices --quiet --json-output".split_whitespace().collect_vec()) .arg(&tmpjson) .stderr(std::process::Stdio::inherit()) .output() .context("Failed to launch xcrun command. Please check that \"xcrun devicectl list devices\" works")?; if !devicectl.status.success() { bail!("xcrun command failed. Please check that \"xcrun devicectl list devices\" works.\n{devicectl:?}"); } let txt = std::fs::read_to_string(&tmpjson) .with_context(|| format!("Reading devicectl json output {tmpjson:?}"))?; for device in json::parse(&txt)?["result"]["devices"].members() { let Some(udid) = device["hardwareProperties"]["udid"] .as_str() .map(|s| s.to_string()) else { continue; }; let device = IosDevice::new( device["deviceProperties"]["name"] .as_str() .context("no name in device json")? .to_string(), udid.clone(), device["hardwareProperties"]["cpuType"]["name"] .as_str() .context("no cpuType in device json")?, device["deviceProperties"]["osVersionNumber"] .as_str() .context("no osVersionNumber")? .to_string(), )?; devices.insert(udid, device); } Ok(()) } fn devices_from_ios_deploy(devices: &mut HashMap) -> Result<()> { let list = ::std::process::Command::new("ios-deploy") .stderr(std::process::Stdio::inherit()) .args(&["-c", "--json", "-t", "1"]) .output(); let list = match list { Ok(l) => l, Err(e) => { info!( "Could not execute ios-deploy to look for legacy (before iOS 17) iOS devices ({}). Consider installing ios-deploy (`brew install ios-deploy`...) for legacy iOS support.", e); return Ok(()); } }; if !list.status.success() { info!( "ios-deploy returned an error while listing devices. It this is not expected, you need to make sure `ios-deploy --json -c -t 1` works as expected. ios-deploy is needed for pre-ios17 devices." ); return Ok(()); } // ios-deploy outputs each device as a multiline json dict, with separator or delimiter. make // it a json array. let list = String::from_utf8(list.stdout)?.replace("}{", "},{"); let list = format!("[{}]", list); let list = ::json::parse(&list) .with_context(|| "Could not parse output for: `ios-deploy --json -c -t 1` as json. Please try to make this command work and retry.")?; for json in list.members() { let device = &json["Device"]; let id = device["DeviceIdentifier"] .as_str() .context("DeviceIdentifier expected to be a string")? .to_owned(); let name = device["DeviceName"] .as_str() .context("DeviceName expected to be a string")? .to_owned(); let arch_cpu = device["modelArch"].as_str().unwrap_or("arm64"); let ios_version = device["ProductVersion"] .as_str() .context("ProductVersion expected to be a string")? .to_string(); devices.insert( name.clone(), IosDevice::new(name, id, &arch_cpu, ios_version)?, ); } Ok(()) } ================================================ FILE: dinghy-lib/src/apple/platform.rs ================================================ use crate::config::PlatformConfiguration; use crate::errors::*; use crate::overlay::Overlayer; use crate::project::Project; use crate::toolchain::Toolchain; use crate::Build; use crate::Device; use crate::Platform; use crate::SetupArgs; use dinghy_build::build_env::set_env; use std::fmt::{Debug, Display, Formatter}; use std::process; use super::AppleSimulatorType; pub struct AppleDevicePlatform { id: String, pub sim: Option, pub toolchain: Toolchain, pub configuration: PlatformConfiguration, } impl Debug for AppleDevicePlatform { fn fmt(&self, fmt: &mut Formatter) -> ::std::fmt::Result { write!(fmt, "{}", self.id) } } impl AppleDevicePlatform { pub fn new( id: String, rustc_triple: &str, simulator: Option, configuration: PlatformConfiguration, ) -> Result> { Ok(Box::new(AppleDevicePlatform { id, sim: simulator, toolchain: Toolchain { rustc_triple: rustc_triple.to_string(), }, configuration, })) } fn sysroot_path(&self) -> Result { let sdk_name = match self.sim { Some(AppleSimulatorType::Ios) => "iphonesimulator", Some(AppleSimulatorType::Tvos) => "appletvsimulator", Some(AppleSimulatorType::Watchos) => "watchsimulator", None => "iphoneos", }; let xcrun = process::Command::new("xcrun") .args(&["--sdk", sdk_name, "--show-sdk-path"]) .output()?; Ok(String::from_utf8(xcrun.stdout)?.trim_end().to_string()) } } impl Platform for AppleDevicePlatform { fn setup_env(&self, project: &Project, setup_args: &SetupArgs) -> Result<()> { let sysroot = self.sysroot_path()?; Overlayer::overlay(&self.configuration, self, project, &self.sysroot_path()?)?; self.toolchain.setup_cc(self.id().as_str(), "gcc")?; set_env("TARGET_SYSROOT", &sysroot); self.toolchain.setup_linker( &self.id(), &format!("cc -isysroot {}", sysroot), &project.metadata.workspace_root, )?; self.toolchain.setup_runner(&self.id(), setup_args)?; self.toolchain.setup_target()?; self.toolchain.setup_pkg_config()?; Ok(()) } fn id(&self) -> String { self.id.to_string() } fn is_compatible_with(&self, device: &dyn Device) -> bool { device.is_compatible_with_simulator_platform(self) } fn is_host(&self) -> bool { false } fn rustc_triple(&self) -> &str { &self.toolchain.rustc_triple } fn strip(&self, build: &mut Build) -> Result<()> { let mut command = ::std::process::Command::new("xcrun"); command.arg("strip"); build.runnable = crate::platform::strip_runnable(&build.runnable, command)?; Ok(()) } fn sysroot(&self) -> Result> { self.sysroot_path().map(|s| Some(s.into())) } } impl Display for AppleDevicePlatform { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> { if self.sim.is_some() { write!(f, "XCode targetting Apple device Simulator") } else { write!(f, "XCode targetting Ios Device") } } } ================================================ FILE: dinghy-lib/src/apple/xcode.rs ================================================ use super::{AppleSimulatorType, SignatureSettings, SigningIdentity}; use crate::errors::*; use fs_err as fs; use log::{debug, trace}; use std::io::Write; use std::{io, process}; use crate::utils::LogCommandExt; use crate::BuildBundle; pub fn add_plist_to_app( bundle: &BuildBundle, arch: &str, app_bundle_id: &str, sim_type: Option<&AppleSimulatorType>, ) -> Result<()> { let mut plist = fs::File::create(bundle.bundle_dir.join("Info.plist"))?; writeln!(plist, r#""#)?; writeln!( plist, r#""# )?; writeln!(plist, r#""#)?; writeln!(plist, "CFBundleNameDinghy",)?; writeln!( plist, "CFBundleExecutableDinghy", )?; writeln!( plist, "CFBundleIdentifier{}", app_bundle_id )?; writeln!(plist, "CFBundleVersion")?; writeln!(plist, "{}", arch)?; writeln!(plist, "CFBundleShortVersionString")?; writeln!(plist, "{}", arch)?; match sim_type { // The iOS/tvOS simulator have the same plist as an iOS device. Some(AppleSimulatorType::Ios) | Some(AppleSimulatorType::Tvos) | None => { writeln!(plist, "UIRequiredDeviceCapabilities")?; writeln!(plist, "{}", arch)?; writeln!(plist, "UILaunchStoryboardName")?; writeln!(plist, "")?; } Some(AppleSimulatorType::Watchos) => { writeln!(plist, "MinimumOSVersion8.0",)?; writeln!(plist, "WKApplication",)?; writeln!(plist, "WKWatchOnly")?; } } writeln!(plist, r#""#)?; Ok(()) } pub fn sign_app(bundle: &BuildBundle, settings: &SignatureSettings) -> Result<()> { debug!( "Will sign {:?} with team: {} using key: {} and profile: {}", bundle.bundle_dir, settings.identity.team, settings.identity.name, settings.file ); let entitlements = bundle.root_dir.join("entitlements.xcent"); debug!("entitlements file: {}", entitlements.to_str().unwrap_or("")); let mut plist = fs::File::create(&entitlements)?; writeln!(plist, r#""#)?; writeln!( plist, r#""# )?; writeln!(plist, r#""#)?; writeln!(plist, "{}", settings.entitlements)?; writeln!(plist, r#""#)?; let result = process::Command::new("codesign") .args(&["-s", &*settings.identity.name, "--entitlements"]) .arg(entitlements) .arg(&bundle.bundle_dir) .log_invocation(2) .status()?; if !result.success() { bail!("Failure to sign application: codesign utility returned non-zero"); } Ok(()) } pub fn look_for_signature_settings(device_id: &str) -> Result> { let identity_regex = ::regex::Regex::new(r#"^ *[0-9]+\) ([A-Z0-9]{40}) "(.+)"$"#)?; let subject_regex = ::regex::Regex::new(r#"OU *= *([^,]+)"#)?; let mut identities: Vec = vec![]; let find_identities = process::Command::new("security") .args(&["find-identity", "-v", "-p", "codesigning"]) .log_invocation(3) .output()?; for line in String::from_utf8(find_identities.stdout)?.split("\n") { if let Some(caps) = identity_regex.captures(&line) { let name: String = caps[2].into(); if !name.starts_with("iPhone Developer: ") && !name.starts_with("Apple Development:") { continue; } let subject = process::Command::new("sh") .arg("-c") .arg(format!( "security find-certificate -a -c \"{}\" -p | openssl x509 -text | \ grep Subject:", name )) .log_invocation(3) .output()?; let subject = String::from_utf8(subject.stdout)?; if let Some(ou) = subject_regex.captures(&subject) { identities.push(SigningIdentity { id: caps[1].into(), name: caps[2].into(), team: ou[1].into(), }) } } } debug!("Possible signing identities: {:?}", identities); let mut settings = vec![]; let home = dirs::home_dir().expect("can't get HOME dir"); let profiles_dir = [ "Library/MobileDevice/Provisioning Profiles", "Library/Developer/Xcode/UserData/Provisioning Profiles", // xcode 16 and above ]; for file in profiles_dir .iter() .filter_map(|path| fs::read_dir(home.join(path)).ok()) .flatten() { let file = file?; if file.path().starts_with(".") || file .path() .extension() .map(|ext| ext.to_string_lossy() != "mobileprovision") .unwrap_or(true) { trace!( " - skipping {:?} (not a mobileprovision profile)", file.path() ); continue; } let decoded = process::Command::new("security") .arg("cms") .arg("-D") .arg("-i") .arg(file.path()) .log_invocation(3) .output()?; let plist = plist::Value::from_reader(io::Cursor::new(&decoded.stdout)) .with_context(|| format!("While reading profile {:?}", file.path()))?; let dict = plist .as_dictionary() .ok_or_else(|| anyhow!("plist root should be a dictionary"))?; let devices = if let Some(d) = dict.get("ProvisionedDevices") { d } else { trace!(" - skipping {:?} (no devices)", file.path()); continue; }; let devices = if let Some(ds) = devices.as_array() { ds } else { bail!("ProvisionedDevices expected to be array") }; if !devices.contains(&plist::Value::String(device_id.into())) { trace!(" - skipping {:?} (not matching target device)", file.path()); continue; } let name = dict .get("Name") .ok_or_else(|| anyhow!(format!("No name in profile {:?}", file.path())))?; let name = name .as_string() .ok_or_else(|| anyhow!("Name should have been a string in {:?}", file.path()))?; if !name.ends_with("Dinghy") && !name.ends_with(" *") { trace!(" - skipping {:?} (wrong app)", file.path()); continue; } // TODO: check date in future let team = dict .get("TeamIdentifier") .ok_or_else(|| anyhow!("no TeamIdentifier"))?; let team = team .as_array() .ok_or_else(|| anyhow!("TeamIdentifier should be an array"))?; let team = team .first() .ok_or_else(|| anyhow!("empty TeamIdentifier"))? .as_string() .ok_or_else(|| anyhow!("TeamIdentifier should be a String"))? .to_string(); let identity = identities.iter().find(|i| i.team == team); if identity.is_none() { trace!( " - skipping {:?} (no identity in profile for team)", file.path() ); continue; } let identity = identity.unwrap(); trace!(" - accepting {:?}", file.path()); let entitlements = String::from_utf8(decoded.stdout)? .split("\n") .skip_while(|line| !line.contains("Entitlements")) .skip(2) .take_while(|line| !line.contains("")) .collect::>() .join("\n"); settings.push(SignatureSettings { entitlements: entitlements, file: file .path() .to_str() .ok_or_else(|| anyhow!("filename should be utf8"))? .into(), name: if name.ends_with(" *") { "org.zoy.kali.Dinghy".into() } else { name.into() }, identity: identity.clone(), profile: file.path().to_str().unwrap().into(), }); } Ok(settings) } ================================================ FILE: dinghy-lib/src/config.rs ================================================ use itertools::Itertools; use serde::de; use serde::{Deserialize, Serialize}; use std::fmt; use std::result; use std::{collections, path}; use crate::errors::*; #[derive(Clone, Debug)] pub struct TestData { pub id: String, pub base: path::PathBuf, pub source: String, pub target: String, pub copy_git_ignored: bool, } #[derive(Serialize, Debug, Clone)] pub struct TestDataConfiguration { pub copy_git_ignored: bool, pub source: String, pub target: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct DetailedTestDataConfiguration { pub source: String, pub copy_git_ignored: bool, pub target: Option, } impl<'de> de::Deserialize<'de> for TestDataConfiguration { fn deserialize(deserializer: D) -> result::Result where D: de::Deserializer<'de>, { struct TestDataVisitor; impl<'de> de::Visitor<'de> for TestDataVisitor { type Value = TestDataConfiguration; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str( "a path like \"tests/my_test_data\" or a \ detailed dependency like { source = \ \"tests/my_test_data\", copy_git_ignored = true }", ) } fn visit_str(self, s: &str) -> result::Result where E: de::Error, { Ok(TestDataConfiguration { copy_git_ignored: false, source: s.to_owned(), target: None, }) } fn visit_map(self, map: V) -> result::Result where V: de::MapAccess<'de>, { let mvd = de::value::MapAccessDeserializer::new(map); let detailed = DetailedTestDataConfiguration::deserialize(mvd)?; Ok(TestDataConfiguration { copy_git_ignored: detailed.copy_git_ignored, source: detailed.source, target: detailed.target, }) } } deserializer.deserialize_any(TestDataVisitor) } } #[derive(Clone, Debug, Default)] pub struct Configuration { pub platforms: collections::BTreeMap, pub ssh_devices: collections::BTreeMap, pub script_devices: collections::BTreeMap, pub test_data: Vec, pub skip_source_copy: bool, } #[derive(Clone, Serialize, Deserialize, Debug, Default)] struct ConfigurationFileContent { pub platforms: Option>, pub ssh_devices: Option>, pub script_devices: Option>, pub test_data: Option>, pub skip_source_copy: Option, } #[derive(Clone, Serialize, Deserialize, Debug, Default)] pub struct PlatformConfiguration { pub deb_multiarch: Option, pub env: Option>, pub overlays: Option>, pub rustc_triple: Option, pub sysroot: Option, pub toolchain: Option, } impl PlatformConfiguration { pub fn empty() -> Self { PlatformConfiguration { deb_multiarch: None, env: None, overlays: None, rustc_triple: None, sysroot: None, toolchain: None, } } pub fn env(&self) -> Vec<(String, String)> { self.env .as_ref() .map(|it| { it.iter() .map(|(key, value)| (key.to_string(), value.to_string())) .collect_vec() }) .unwrap_or(vec![]) } } #[derive(Clone, Serialize, Deserialize, Debug, Default)] pub struct OverlayConfiguration { pub path: String, pub scope: Option, } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct SshDeviceConfiguration { pub hostname: String, pub username: String, pub port: Option, pub path: Option, pub target: Option, pub toolchain: Option, pub platform: Option, #[serde(default)] pub remote_shell_vars: collections::HashMap, pub install_adhoc_rsync_local_path: Option, pub use_legacy_scp_protocol_for_adhoc_rsync_copy: Option, } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct ScriptDeviceConfiguration { pub path: String, pub platform: Option, } impl Configuration { pub fn merge(&mut self, file: &path::Path) -> Result<()> { let other = read_config_file(&file)?; if let Some(pfs) = other.platforms { self.platforms.extend(pfs) } self.ssh_devices .extend(other.ssh_devices.unwrap_or(collections::BTreeMap::new())); self.script_devices .extend(other.script_devices.unwrap_or(collections::BTreeMap::new())); for (id, source) in other.test_data.unwrap_or(collections::BTreeMap::new()) { // TODO Remove key self.test_data.push(TestData { id: id.to_string(), base: file.to_path_buf(), source: source.source.clone(), target: source.target.unwrap_or(source.source.clone()), copy_git_ignored: source.copy_git_ignored, }) } if let Some(skip_source_copy) = other.skip_source_copy { self.skip_source_copy = skip_source_copy } Ok(()) } } fn read_config_file>(file: P) -> Result { let file = file.as_ref(); let data = std::fs::read_to_string(file).with_context(|| format!("Reading {file:?}"))?; Ok(toml::from_str(&data)?) } pub fn dinghy_config>(dir: P) -> Result { let mut conf = Configuration::default(); let mut files_to_try = vec![]; let dir = dir.as_ref().to_path_buf(); let mut d = dir.as_path(); while d.parent().is_some() { files_to_try.push(d.join("dinghy.toml")); files_to_try.push(d.join(".dinghy.toml")); files_to_try.push(d.join(".dinghy").join("dinghy.toml")); files_to_try.push(d.join(".dinghy").join(".dinghy.toml")); d = d.parent().unwrap(); } files_to_try.push(d.join(".dinghy.toml")); if let Some(home) = dirs::home_dir() { if !dir.starts_with(&home) { files_to_try.push(home.join("dinghy.toml")); files_to_try.push(home.join(".dinghy.toml")); files_to_try.push(home.join(".dinghy").join("dinghy.toml")); files_to_try.push(home.join(".dinghy").join(".dinghy.toml")); } } for file in files_to_try { if path::Path::new(&file).exists() { log::debug!("Loading configuration from {:?}", file); conf.merge(&file)?; } else { log::trace!("No configuration found at {:?}", file); } } log::debug!("Configuration: {:#?}", conf); Ok(conf) } #[cfg(test)] mod tests { #[test] fn load_config_with_str_test_data() { let config_file = ::std::env::current_exe() .unwrap() .parent() .unwrap() .join("../../../test-ws/test-app/.dinghy.toml"); super::read_config_file(config_file).unwrap(); } } ================================================ FILE: dinghy-lib/src/device.rs ================================================ use crate::errors::*; use crate::project; use crate::project::{rec_copy, Project}; use crate::utils::copy_and_sync_file; use crate::Build; use crate::BuildBundle; use fs_err as fs; use log::debug; use std::path::Path; pub fn make_remote_app(project: &Project, build: &Build) -> Result { make_remote_app_with_name(project, build, None) } pub fn make_remote_app_with_name( project: &Project, build: &Build, bundle_name: Option<&str>, ) -> Result { fn is_sysroot_library(path: &Path) -> bool { path.ancestors() .find(|ancestor_path| ancestor_path.ends_with("sysroot/usr/lib")) .is_some() && (!path .file_name() .unwrap() .to_str() .unwrap() .eq_ignore_ascii_case("libc++_shared.so") && !path.to_str().unwrap().contains("android")) } let root_dir = build.target_path.join("dinghy").join(build.runnable.id.clone()); let bundle_path = match bundle_name { Some(name) => root_dir.join(&build.runnable.package_name).join(name), None => root_dir.join(&build.runnable.package_name), }; let bundle_libs_path = root_dir.join("overlay"); let bundle_target_path = &bundle_path; let bundle_exe_path = bundle_target_path.join(format!("_dinghy_{}", &build.runnable.id)); debug!("Removing previous bundle {:?}", bundle_path); let _ = fs::remove_dir_all(&bundle_path); let _ = fs::remove_dir_all(&bundle_libs_path); let _ = fs::remove_dir_all(&bundle_target_path); debug!("Making bundle {:?}", bundle_path); fs::create_dir_all(&bundle_path)?; fs::create_dir_all(&bundle_libs_path)?; fs::create_dir_all(&bundle_target_path)?; debug!( "Copying exe {:?} to bundle {:?}", &build.runnable.exe, bundle_exe_path ); copy_and_sync_file(&build.runnable.exe, &bundle_exe_path).with_context(|| { format!( "Couldn't copy {} to {}", &build.runnable.exe.display(), &bundle_exe_path.display() ) })?; debug!("Copying dynamic libs to bundle"); for src_lib_path in &build.dynamic_libraries { let target_lib_path = bundle_libs_path.join( src_lib_path .file_name() .ok_or_else(|| anyhow!("Invalid file name {:?}", src_lib_path.file_name()))?, ); if !is_sysroot_library(&src_lib_path) { debug!( "Copying dynamic lib {} to {}", src_lib_path.display(), target_lib_path.display() ); copy_and_sync_file(&src_lib_path, &target_lib_path).with_context(|| { format!( "Couldn't copy {} to {}", src_lib_path.display(), &target_lib_path.display() ) })?; } else { debug!( "Dynamic lib {} will not be copied as it is a sysroot library", src_lib_path.display() ); } } for file_in_run_args in &build.files_in_run_args { let dst = bundle_target_path.join( file_in_run_args .file_name() .ok_or_else(|| anyhow!("no file name"))?, ); if file_in_run_args.is_dir() { rec_copy(file_in_run_args, dst, true)?; } else { copy_and_sync_file(&file_in_run_args, &dst).with_context(|| { format!( "Couldn't copy {} to {}", file_in_run_args.display(), &root_dir.display() ) })?; } } if !build.runnable.skip_source_copy { debug!( "Copying src {} to bundle {}", build.runnable.source.display(), bundle_path.display() ); project::rec_copy_excl( &build.runnable.source, &bundle_path, false, &[build.runnable.source.join("target")], )?; } else { debug!("Skipping source copy to bundle {}", bundle_path.display()); } debug!("Copying test_data to bundle {}", bundle_path.display()); project.copy_test_data(&bundle_path)?; Ok(BuildBundle { id: build.runnable.id.clone(), bundle_dir: bundle_path.to_path_buf(), bundle_exe: bundle_exe_path.to_path_buf(), lib_dir: bundle_libs_path.to_path_buf(), root_dir, app_id: None, }) } ================================================ FILE: dinghy-lib/src/host/mod.rs ================================================ use crate::{Configuration, Device, Platform, PlatformConfiguration, PlatformManager, Result}; mod platform; pub use self::platform::HostPlatform; pub struct HostManager { host_conf: PlatformConfiguration, } impl HostManager { pub fn probe(conf: &Configuration) -> Option { let host_conf = conf .platforms .get("host") .map(|it| (*it).clone()) .unwrap_or(PlatformConfiguration::empty()); Some(HostManager { host_conf }) } fn platform(&self) -> Result { platform::HostPlatform::new(self.host_conf.clone()) } } impl PlatformManager for HostManager { fn devices(&self) -> Result>> { Ok(vec![]) } fn platforms(&self) -> Result>> { Ok(vec![Box::new(self.platform()?)]) } } ================================================ FILE: dinghy-lib/src/host/platform.rs ================================================ use crate::config::PlatformConfiguration; use crate::overlay::Overlayer; use crate::platform; use crate::project::Project; use crate::utils::LogCommandExt; use crate::Build; use crate::Device; use crate::Platform; use crate::Result; use crate::SetupArgs; use anyhow::anyhow; use dinghy_build::build_env::{envify, set_all_env, set_env}; use std::fmt::{Debug, Formatter}; use std::io::BufRead; use std::process::{Command, Stdio}; #[derive(Clone)] pub struct HostPlatform { pub configuration: PlatformConfiguration, pub id: String, } impl HostPlatform { pub fn new(configuration: PlatformConfiguration) -> Result { Ok(HostPlatform { configuration, id: "host".to_string(), }) } } impl Debug for HostPlatform { fn fmt(&self, fmt: &mut Formatter) -> ::std::fmt::Result { write!(fmt, "{}", self.id) } } impl Platform for HostPlatform { fn setup_env(&self, project: &Project, setup_args: &SetupArgs) -> Result<()> { // Set custom env variables specific to the platform set_all_env(&self.configuration.env()); let triple = std::process::Command::new("rustc") .arg("-vV") .stdout(Stdio::piped()) .log_invocation(3) .output()? .stdout .lines() .find_map(|line| { line.ok() .and_then(|line| line.strip_prefix("host: ").map(ToString::to_string)) }) .ok_or_else(|| anyhow!("could not get host triple from rustc"))?; set_env( format!("CARGO_TARGET_{}_RUNNER", envify(triple)), setup_args.get_runner_command("host"), ); Overlayer::overlay(&self.configuration, self, project, "/")?; Ok(()) } fn id(&self) -> String { "host".to_string() } fn is_compatible_with(&self, device: &dyn Device) -> bool { device.is_compatible_with_host_platform(self) } fn is_host(&self) -> bool { true } fn rustc_triple(&self) -> &str { std::env!("TARGET") } fn strip(&self, build: &mut Build) -> Result<()> { log::info!("Stripping {}", build.runnable.exe.display()); build.runnable = platform::strip_runnable(&build.runnable, Command::new("strip"))?; Ok(()) } fn sysroot(&self) -> Result> { Ok(Some(std::path::PathBuf::from("/"))) } } ================================================ FILE: dinghy-lib/src/lib.rs ================================================ #![type_length_limit = "2149570"] pub mod errors { pub use anyhow::{anyhow, bail, Context, Error, Result}; } mod android; #[cfg(target_os = "macos")] mod apple; pub mod config; pub mod device; mod host; pub mod overlay; pub mod platform; pub mod plugin; pub mod project; mod script; mod ssh; mod toolchain; pub mod utils; pub use crate::config::Configuration; #[cfg(target_os = "macos")] use crate::apple::{IosManager, TvosManager, WatchosManager}; use crate::config::PlatformConfiguration; use crate::platform::regular_platform::RegularPlatform; use crate::project::Project; use anyhow::{anyhow, Context}; use dyn_clone::DynClone; use std::fmt::Display; use std::{path, sync}; use crate::errors::Result; pub struct Dinghy { devices: Vec>>, platforms: Vec<(String, sync::Arc>)>, } impl Dinghy { pub fn probe(conf: &sync::Arc) -> Result { let mut managers: Vec> = vec![]; if let Some(man) = host::HostManager::probe(conf) { managers.push(Box::new(man)); } if let Some(man) = android::AndroidManager::probe() { managers.push(Box::new(man)); } if let Some(man) = script::ScriptDeviceManager::probe(conf.clone()) { managers.push(Box::new(man)); } if let Some(man) = ssh::SshDeviceManager::probe(conf.clone()) { managers.push(Box::new(man)); } #[cfg(target_os = "macos")] { std::thread::sleep(std::time::Duration::from_millis(100)); if let Some(man) = IosManager::new().context("Could not initialize iOS manager")? { managers.push(Box::new(man)); } if let Some(man) = TvosManager::new().context("Could not initialize tvOS manager")? { managers.push(Box::new(man)); } if let Some(man) = WatchosManager::new().context("Could not initialize tvOS manager")? { managers.push(Box::new(man)); } } if let Some(man) = plugin::PluginManager::probe(conf.clone()) { managers.push(Box::new(man)); } let mut devices = vec![]; let mut platforms = vec![]; for man in managers.into_iter() { devices.extend( man.devices() .context("Could not list devices")? .into_iter() .map(|it| sync::Arc::new(it)), ); platforms.extend( man.platforms() .context("Could not list platforms")? .into_iter() .map(|it| (it.id(), sync::Arc::new(it))), ); } for (platform_name, platform_conf) in &conf.platforms { if platform_name == "host" { continue; } let rustc_triple = platform_conf .rustc_triple .as_ref() .ok_or_else(|| anyhow!("Platform {} has no rustc_triple", platform_name))?; let pf = RegularPlatform::new( platform_conf.clone(), platform_name.to_string(), rustc_triple.clone(), platform_conf .toolchain .clone() .map(|it| path::PathBuf::from(it)) .or(dirs::home_dir() .map(|it| it.join(".dinghy").join("toolchain").join(platform_name))) .with_context(|| format!("Toolchain missing for platform {}", platform_name))?, ) .with_context(|| format!("Could not assemble platform {}", platform_name))?; platforms.push((pf.id(), sync::Arc::new(pf))); } Ok(Dinghy { devices, platforms }) } pub fn devices(&self) -> Vec>> { self.devices.clone() } pub fn host_platform(&self) -> sync::Arc> { self.platforms[0].1.clone() } pub fn platforms(&self) -> Vec>> { self.platforms .iter() .map(|&(_, ref platform)| platform.clone()) .collect() } pub fn platform_by_name( &self, platform_name_filter: &str, ) -> Option>> { self.platforms .iter() .filter(|&&(ref platform_name, _)| platform_name == platform_name_filter) .map(|&(_, ref platform)| platform.clone()) .next() } } pub trait Device: std::fmt::Debug + Display + DeviceCompatibility + DynClone { fn clean_app(&self, build_bundle: &BuildBundle) -> Result<()>; fn debug_app( &self, project: &Project, build: &Build, args: &[&str], envs: &[&str], ) -> Result; fn id(&self) -> &str; fn name(&self) -> &str; fn run_app( &self, project: &Project, build: &Build, args: &[&str], envs: &[&str], ) -> Result; } dyn_clone::clone_trait_object!(Device); pub trait DeviceCompatibility { fn is_compatible_with_regular_platform(&self, _platform: &RegularPlatform) -> bool { false } fn is_compatible_with_host_platform(&self, _platform: &host::HostPlatform) -> bool { false } #[cfg(target_os = "macos")] fn is_compatible_with_simulator_platform( &self, _platform: &apple::AppleDevicePlatform, ) -> bool { false } } pub trait Platform: std::fmt::Debug { fn setup_env(&self, project: &Project, setup_args: &SetupArgs) -> Result<()>; fn id(&self) -> String; fn is_compatible_with(&self, device: &dyn Device) -> bool; fn is_host(&self) -> bool; fn rustc_triple(&self) -> &str; fn strip(&self, build: &mut Build) -> Result<()>; fn sysroot(&self) -> Result>; } impl Display for dyn Platform { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { write!(fmt, "{}", self.id()) } } pub trait PlatformManager { fn devices(&self) -> Result>>; fn platforms(&self) -> Result>>; } #[derive(Clone, Debug)] pub struct Build { pub setup_args: SetupArgs, pub dynamic_libraries: Vec, pub runnable: Runnable, pub target_path: path::PathBuf, pub files_in_run_args: Vec, } #[derive(Clone, Debug)] pub struct SetupArgs { pub verbosity: i8, pub forced_overlays: Vec, pub envs: Vec, pub cleanup: bool, pub strip: bool, pub device_id: Option, } impl SetupArgs { pub fn get_runner_command(&self, platform_id: &str) -> String { let mut extra_args = String::new(); if self.verbosity > 0 { for _ in 0..self.verbosity { extra_args.push_str("-v ") } } if self.verbosity < 0 { for _ in 0..-self.verbosity { extra_args.push_str("-q ") } } if self.cleanup { extra_args.push_str("--cleanup ") } if self.strip { extra_args.push_str("--strip ") } if let Some(device_id) = &self.device_id { extra_args.push_str("-d "); extra_args.push_str(&device_id); extra_args.push(' '); } for env in &self.envs { extra_args.push_str("-e "); extra_args.push_str(env); extra_args.push(' '); } format!( "{} -p {} {}runner --", std::env::current_exe().unwrap().to_str().unwrap(), platform_id, extra_args ) } } #[derive(Clone, Debug, Default)] pub struct BuildBundle { pub id: String, pub bundle_dir: path::PathBuf, pub bundle_exe: path::PathBuf, pub lib_dir: path::PathBuf, pub root_dir: path::PathBuf, pub app_id: Option, } impl BuildBundle { fn replace_prefix_with>(&self, path: P) -> Result { Ok(BuildBundle { bundle_dir: utils::normalize_path( &path .as_ref() .to_path_buf() .join(self.bundle_dir.strip_prefix(&self.root_dir)?), ), bundle_exe: utils::normalize_path( &path .as_ref() .to_path_buf() .join(self.bundle_exe.strip_prefix(&self.root_dir)?), ), lib_dir: utils::normalize_path( &path .as_ref() .to_path_buf() .join(self.lib_dir.strip_prefix(&self.root_dir)?), ), root_dir: path.as_ref().to_path_buf(), ..self.clone() }) } } #[derive(Clone, Debug, Default)] pub struct Runnable { pub id: String, pub package_name: String, pub exe: path::PathBuf, pub source: path::PathBuf, pub skip_source_copy: bool, } ================================================ FILE: dinghy-lib/src/overlay.rs ================================================ use crate::config::PlatformConfiguration; use crate::errors::*; use crate::project::Project; use crate::utils::contains_file_with_ext; use crate::utils::destructure_path; use crate::utils::file_has_ext; use crate::utils::lib_name_from; use crate::Platform; use dinghy_build::build_env::append_path_to_target_env; use dinghy_build::build_env::envify; use dinghy_build::build_env::set_env_ifndef; use dinghy_build::utils::path_between; use dirs::home_dir; use fs_err::{create_dir_all, read_dir, remove_dir_all, File}; use itertools::Itertools; use std::io::Write; use std::path::Path; use std::path::PathBuf; use walkdir::WalkDir; #[derive(Clone, Debug)] pub enum OverlayScope { Application, System, } #[derive(Clone, Debug)] pub struct Overlay { pub id: String, pub path: PathBuf, pub scope: OverlayScope, } #[derive(Clone, Debug)] pub struct Overlayer { platform_id: String, rustc_triple: Option, sysroot: PathBuf, work_dir: PathBuf, } impl Overlayer { pub fn overlay>( configuration: &PlatformConfiguration, platform: &dyn Platform, project: &Project, sysroot: P, ) -> Result<()> { let overlayer = Overlayer { platform_id: platform.id().to_string(), rustc_triple: Some(platform.rustc_triple().to_string()), sysroot: sysroot.as_ref().to_path_buf(), work_dir: project.overlay_work_dir(platform)?, }; let mut path_to_try = vec![]; let project_path = project.project_dir()?; let mut current_path = project_path.as_path(); while current_path.parent().is_some() { path_to_try.push( current_path .join(".dinghy") .join("overlay") .join(&overlayer.platform_id), ); if let Some(parent_path) = current_path.parent() { current_path = parent_path; } else { break; } } // Project may be outside home directory. So add it too. if let Some(dinghy_home_dir) = home_dir().map(|it| { it.join(".dinghy") .join("overlay") .join(&overlayer.platform_id) }) { if !path_to_try.contains(&dinghy_home_dir) { path_to_try.push(dinghy_home_dir) } } overlayer.apply_overlay( Overlayer::from_conf(configuration)? .into_iter() .chain(path_to_try.into_iter().flat_map(|path_to_try| { Overlayer::from_directory(path_to_try).unwrap_or_default() })) .unique_by(|overlay| overlay.id.clone()) .collect_vec(), ) } fn from_conf(configuration: &PlatformConfiguration) -> Result> { Ok(configuration .overlays .as_ref() .unwrap_or(&::std::collections::HashMap::new()) .into_iter() .map(|(overlay_id, overlay_conf)| Overlay { id: overlay_id.to_string(), path: PathBuf::from(overlay_conf.path.as_str()), scope: OverlayScope::Application, }) .collect()) } fn from_directory>(overlay_root_dir: P) -> Result> { Ok(read_dir(overlay_root_dir.as_ref())? .filter_map(|it| it.ok()) // Ignore invalid directories .map(|it| it.path()) .filter(|it| it.is_dir()) .filter_map(destructure_path) .map(|(overlay_dir_path, overlay_dir_name)| Overlay { id: overlay_dir_name, path: overlay_dir_path.to_path_buf(), scope: OverlayScope::Application, }) .collect()) } fn apply_overlay(&self, overlays: I) -> Result<()> where I: IntoIterator, { let pkg_config_env_var = self .rustc_triple .as_ref() .map(|_| "PKG_CONFIG_LIBDIR") // Fallback on PKG_CONFIG_LIBPATH for host as it doesn't erase pkg_config paths .unwrap_or("PKG_CONFIG_LIBPATH"); // Setup overlay work directory if let Err(error) = remove_dir_all(&self.work_dir) { if self.work_dir.exists() { log::warn!( "Couldn't cleanup directory overlay work directory {} ({:?})", self.work_dir.display(), error ) } } create_dir_all(&self.work_dir)?; append_path_to_target_env( pkg_config_env_var, self.rustc_triple.as_ref(), &self.work_dir, ); for overlay in overlays { log::debug!("Overlaying '{}'", overlay.id.as_str()); let mut has_pkg_config_files = false; let pkg_config_path_list = WalkDir::new(&overlay.path) .into_iter() .filter_map(|entry| entry.ok()) // Ignore unreadable directories, maybe could warn... .filter(|entry| entry.file_type().is_dir()) .filter(|dir| { dir.file_name() == "pkgconfig" || contains_file_with_ext(dir.path(), ".pc") }) .map(|pkg_config_path| pkg_config_path.path().to_path_buf()); for pkg_config_path in pkg_config_path_list { log::debug!( "Discovered pkg-config directory '{}'", pkg_config_path.display() ); append_path_to_target_env( pkg_config_env_var, self.rustc_triple.as_ref(), pkg_config_path, ); has_pkg_config_files = true; } if !has_pkg_config_files { self.generate_pkg_config_file(&overlay)?; append_path_to_target_env( pkg_config_env_var, self.rustc_triple.as_ref(), &overlay.path, ); } // Override the 'prefix' pkg-config variable for the specified overlay only. set_env_ifndef( envify(format!("PKG_CONFIG_{}_PREFIX", overlay.id)), path_between(&self.sysroot, &overlay.path), ); } Ok(()) } fn generate_pkg_config_file(&self, overlay: &Overlay) -> Result<()> { fn write_pkg_config_file, T: AsRef>( pc_file_path: P, name: &str, libs: &[T], ) -> Result<()> { log::debug!( "Generating pkg-config pc file {}", pc_file_path.as_ref().display() ); let mut pc_file = File::create(pc_file_path.as_ref())?; pc_file.write_all(b"prefix:/")?; pc_file.write_all(b"\nexec_prefix:${prefix}")?; pc_file.write_all(b"\nName: ")?; pc_file.write_all(name.as_bytes())?; pc_file.write_all(b"\nDescription: ")?; pc_file.write_all(name.as_bytes())?; pc_file.write_all(b"\nVersion: unspecified")?; pc_file.write_all(b"\nLibs: -L${prefix} ")?; for lib in libs { pc_file.write_all(b" -l")?; pc_file.write_all(lib.as_ref().as_bytes())?; } pc_file.write_all(b"\nCflags: -I${prefix}")?; Ok(()) } let pc_file = self.work_dir.join(format!("{}.pc", self.platform_id)); let lib_list = WalkDir::new(&overlay.path) .max_depth(1) .into_iter() .filter_map(|entry| entry.ok()) // Ignore unreadable files, maybe could warn... .filter(|entry| file_has_ext(entry.path(), ".so")) .filter_map(|e| lib_name_from(e.path()).ok()) .collect_vec(); write_pkg_config_file(pc_file.as_path(), overlay.id.as_str(), &lib_list).with_context( || { format!( "Dinghy couldn't generate pkg-config pc file {}", pc_file.as_path().display() ) }, ) } } ================================================ FILE: dinghy-lib/src/platform/mod.rs ================================================ use crate::utils::{file_name_as_str, LogCommandExt}; use crate::Result; use crate::Runnable; use std::fs; use std::process::Command; use anyhow::{bail, Context}; use log::debug; pub mod regular_platform; pub fn strip_runnable(runnable: &Runnable, mut command: Command) -> Result { let exe_stripped_name = file_name_as_str(&runnable.exe)?; let mut stripped_runnable = runnable.clone(); stripped_runnable.exe = runnable .exe .parent() .map(|it| it.join(format!("{}-stripped", exe_stripped_name))) .with_context(|| format!("{} is not a valid executable name", &runnable.exe.display()))?; // Backup old runnable fs::copy(&runnable.exe, &stripped_runnable.exe)?; let command = command.arg(&stripped_runnable.exe); debug!("Running command {:?}", command); let output = command.log_invocation(2).output()?; if !output.status.success() { bail!( "Error while stripping {}\nError: {}", &stripped_runnable.exe.display(), String::from_utf8(output.stdout)? ) } debug!( "{} unstripped size = {} and stripped size = {}", runnable.exe.display(), fs::metadata(&runnable.exe)?.len(), fs::metadata(&stripped_runnable.exe)?.len() ); Ok(stripped_runnable) } ================================================ FILE: dinghy-lib/src/platform/regular_platform.rs ================================================ use crate::config::PlatformConfiguration; use crate::overlay::Overlayer; use crate::platform; use crate::project::Project; use crate::toolchain::ToolchainConfig; use crate::Build; use crate::Device; use crate::Platform; use crate::Result; use crate::SetupArgs; use dinghy_build::build_env::set_all_env; use std::fmt::{Debug, Display, Formatter}; use std::path::Path; use std::path::PathBuf; use std::process::Command; use anyhow::anyhow; use fs_err::read_dir; use log::trace; pub struct RegularPlatform { pub configuration: PlatformConfiguration, pub id: String, pub toolchain: ToolchainConfig, } impl Debug for RegularPlatform { fn fmt(&self, fmt: &mut Formatter) -> ::std::fmt::Result { write!(fmt, "{}", self.id) } } impl RegularPlatform { pub fn new>( configuration: PlatformConfiguration, id: String, rustc_triple: String, toolchain_path: P, ) -> Result> { if let Some(prefix) = configuration.deb_multiarch.clone() { return Ok(Box::new(RegularPlatform { configuration, id, toolchain: ToolchainConfig { bin_dir: "/usr/bin".into(), rustc_triple, root: "/".into(), sysroot: Some("/".into()), cc: "gcc".to_string(), cxx: "c++".to_string(), binutils_prefix: prefix.clone(), cc_prefix: prefix.clone(), }, })); } let toolchain_path = toolchain_path.as_ref(); let toolchain_bin_path = toolchain_path.join("bin"); let mut bin: Option = None; let mut prefix: Option = None; for file in read_dir(&toolchain_bin_path)? { let file = file?; if file.file_name().to_string_lossy().ends_with("-gcc") || file.file_name().to_string_lossy().ends_with("-gcc.exe") { bin = Some(toolchain_bin_path); prefix = Some( file.file_name() .to_string_lossy() .replace(".exe", "") .replace("-gcc", ""), ); break; } } let bin_dir = bin.ok_or_else(|| anyhow!("no bin/*-gcc found in toolchain"))?; let tc_triple = prefix .ok_or_else(|| anyhow!("no gcc in toolchain"))? .to_string(); let sysroot = find_sysroot(&toolchain_path)?; let toolchain = ToolchainConfig { bin_dir, rustc_triple, root: toolchain_path.into(), sysroot, cc: "gcc".to_string(), cxx: "c++".to_string(), binutils_prefix: tc_triple.clone(), cc_prefix: tc_triple, }; Self::new_with_tc(configuration, id, toolchain) } pub fn new_with_tc( configuration: PlatformConfiguration, id: String, toolchain: ToolchainConfig, ) -> Result> { Ok(Box::new(RegularPlatform { configuration, id, toolchain, })) } } impl Display for RegularPlatform { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> { write!(f, "{:?}", self.toolchain.root) } } impl Platform for RegularPlatform { fn setup_env(&self, project: &Project, setup_args: &SetupArgs) -> Result<()> { // Cleanup environment set_all_env(&[("LIBRARY_PATH", ""), ("LD_LIBRARY_PATH", "")]); // Set custom env variables specific to the platform set_all_env(&self.configuration.env()); if let Some(sr) = &self.toolchain.sysroot { Overlayer::overlay(&self.configuration, self, project, &sr)?; } self.toolchain .setup_cc(&self.id, &self.toolchain.cc_executable(&self.toolchain.cc))?; if Path::new(&self.toolchain.binutils_executable("ar")).exists() { self.toolchain .setup_tool("AR", &self.toolchain.binutils_executable("ar"))?; } if Path::new(&self.toolchain.binutils_executable("as")).exists() { self.toolchain .setup_tool("AS", &self.toolchain.binutils_executable("as"))?; } if Path::new(&self.toolchain.cc_executable(&self.toolchain.cxx)).exists() { self.toolchain .setup_tool("CXX", &self.toolchain.cc_executable(&self.toolchain.cxx))?; } if Path::new(&self.toolchain.cc_executable("cpp")).exists() { self.toolchain .setup_tool("CPP", &self.toolchain.cc_executable("cpp"))?; } if Path::new(&self.toolchain.binutils_executable("gfortran")).exists() { self.toolchain .setup_tool("FC", &self.toolchain.binutils_executable("gfortran"))?; } trace!("Setup linker..."); self.toolchain.setup_linker( &self.id, &self.toolchain.generate_linker_command(&setup_args), &project.metadata.workspace_root, )?; trace!("Setup pkg-config"); self.toolchain.setup_pkg_config()?; trace!("Setup sysroot..."); self.toolchain.setup_sysroot(); trace!("Setup shims..."); self.toolchain .shim_executables(&self.id, &project.metadata.workspace_root)?; trace!("Setup runner..."); self.toolchain.setup_runner(&self.id, setup_args)?; trace!("Setup target..."); self.toolchain.setup_target()?; Ok(()) } fn id(&self) -> String { self.id.clone() } fn is_compatible_with(&self, device: &dyn Device) -> bool { device.is_compatible_with_regular_platform(self) } fn is_host(&self) -> bool { false } fn rustc_triple(&self) -> &str { &self.toolchain.rustc_triple } fn strip(&self, build: &mut Build) -> Result<()> { build.runnable = platform::strip_runnable( &build.runnable, Command::new(self.toolchain.binutils_executable("strip")), )?; Ok(()) } fn sysroot(&self) -> Result> { Ok(self.toolchain.sysroot.clone()) } } fn find_sysroot>(toolchain_path: P) -> Result> { let toolchain = toolchain_path.as_ref(); let immediate = toolchain.join("sysroot"); if immediate.is_dir() { let sysroot = immediate .to_str() .ok_or_else(|| anyhow!("sysroot is not utf-8"))?; return Ok(Some(sysroot.into())); } for subdir in toolchain.read_dir()? { let subdir = subdir?; let maybe = subdir.path().join("sysroot"); if maybe.is_dir() { let sysroot = maybe .to_str() .ok_or_else(|| anyhow!("sysroot is not utf-8"))?; return Ok(Some(sysroot.into())); } } Ok(None) } ================================================ FILE: dinghy-lib/src/plugin/mod.rs ================================================ use crate::config::{PlatformConfiguration, ScriptDeviceConfiguration, SshDeviceConfiguration}; use crate::platform::regular_platform::RegularPlatform; use crate::{Configuration, Device, Platform, PlatformManager}; use anyhow::{anyhow, bail, Context, Result}; use log::debug; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::os::unix::fs::PermissionsExt; use std::process::Command; use std::sync::Arc; use std::{env, fs}; /// This platform manager will auto-detect any executable in the PATH that starts with /// `cargo-dinghy-` and try to use them as a plugin to provide devices and platforms. /// /// To be a valid plugin, an executable must implement the following subcommands: /// - `devices`: must output a TOML file with a `DevicePluginOutput` structure /// - `platforms`: must output a TOML file with a `BTreeMap` structure /// /// Here is example of output for a `cargo-dinghy-foo` plugin configuring a `bar` device and a `baz` /// platform: /// /// ```no_compile /// $ cargo-dinghy-foo devices /// [ssh_devices.bar] /// hostname = "127.0.0.1" /// username = "user" /// /// $ cargo-dinghy-foo platforms /// [baz] /// rustc_triple = "aarch64-unknown-linux-gnu" /// toolchain = "/path/to/toolchain" /// ``` /// This is quite useful if you have a bench of devices and platforms that can be auto-detected /// or are already configured in another tool. pub struct PluginManager { conf: Arc, auto_detected_plugins: Vec, } impl PluginManager { pub fn probe(conf: Arc) -> Option { let auto_detected_plugins = auto_detect_plugins(); if auto_detected_plugins.is_empty() { debug!("No auto-detected plugins found"); None } else { debug!("Auto-detected plugins: {:?}", auto_detected_plugins); Some(Self { conf, auto_detected_plugins, }) } } fn create_script_devices( &self, provider: &String, script_devices: BTreeMap, ) -> Vec> { script_devices .into_iter() .filter_map(|(id, conf)| { if self.conf.script_devices.get(&id).is_none() { debug!("registering script device {id} from {provider}"); Some(Box::new(crate::script::ScriptDevice { id, conf }) as _) } else { debug!("ignoring script device {id} from {provider} as is was already registered in configuration"); None } }) .collect() } fn create_ssh_devices( &self, provider: &String, ssh_devices: BTreeMap, ) -> Vec> { ssh_devices.into_iter().filter_map(|(id, conf)| { if self.conf.script_devices.get(&id).is_none() { debug!("registering ssh device {id} from {provider}"); Some(Box::new(crate::ssh::SshDevice { id, conf, }) as _) } else { debug!("ignoring ssh device {id} from {provider} as is was already registered in configuration"); None } }).collect() } } impl PlatformManager for PluginManager { fn devices(&self) -> Result>> { let mut result: Vec> = vec![]; self.auto_detected_plugins.iter().for_each(|provider| { match get_devices_from_plugin(provider) { Ok(DevicePluginOutput{script_devices, ssh_devices}) => { if let Some(script_devices) = script_devices { result.append(&mut self.create_script_devices(provider, script_devices)) } if let Some(ssh_devices) = ssh_devices { result.append(&mut self.create_ssh_devices(provider, ssh_devices)) } } Err(e) => { debug!( "failed to get devices from auto detected script provider: {provider}, {e:?}", ); } } }); Ok(result) } fn platforms(&self) -> anyhow::Result>> { let mut script_platforms = BTreeMap::new(); self.auto_detected_plugins.iter().for_each( |provider| match get_platforms_from_plugin(provider) { Ok(platforms) => { platforms.into_iter().for_each(|(id, platform)| { if script_platforms.get(&id).is_none() && self.conf.platforms.get(&id).is_none() { debug!("registering platform {id} from {provider}"); script_platforms.insert(id.clone(), platform); } else { debug!( "ignoring platform {id} from plugin {provider} as is was already registered" ); } }); } Err(e) => { debug!( "failed to get platforms from auto detected script provider: {provider}, {:?}", e ); } }, ); Ok(script_platforms.into_values().collect()) } } #[derive(Debug, Serialize, Deserialize)] pub struct DevicePluginOutput { pub ssh_devices: Option>, pub script_devices: Option>, } fn get_devices_from_plugin(plugin: &str) -> Result { let output = Command::new(plugin).arg("devices").output()?; if !output.status.success() { bail!("failed to get devices from auto detected script provider: {:?}, non success return code", plugin); } Ok(toml::from_str( &String::from_utf8(output.stdout) .with_context(|| format!("Failed to parse string output from {plugin} devices"))?, ) .with_context(|| format!("Failed to parse toml output from {plugin} devices"))?) } fn get_platforms_from_plugin(plugin: &str) -> Result>> { let output = Command::new(plugin).arg("platforms").output()?; if !output.status.success() { bail!("failed to get platforms from auto detected script provider: {:?}, non success return code", plugin); } let platform_configs = toml::from_str::>( &String::from_utf8(output.stdout) .with_context(|| format!("Failed to parse string output from {plugin} platforms"))?, ) .with_context(|| format!("Failed to parse toml output from {plugin} platforms"))?; platform_configs .into_iter() .map(|(name, conf)| { let triple = conf .rustc_triple .clone() .ok_or_else(|| anyhow!("Platform {name} from {plugin} has no rustc_triple"))?; let toolchain = conf .toolchain .clone() .ok_or_else(|| anyhow!("Toolchain missing for platform {name} from {plugin}"))?; Ok(( name.clone(), RegularPlatform::new(conf, name, triple, toolchain)?, )) }) .collect() } // dinghy will auto-detect any executable in the PATH that starts with `cargo-dinghy-` and try to // use it as a plugin. fn auto_detect_plugins() -> Vec { let mut binaries = Vec::new(); if let Some(paths) = env::var_os("PATH") { for path in env::split_paths(&paths) { if let Ok(entries) = fs::read_dir(&path) { for entry in entries.filter_map(|e| e.ok()) { let path = entry.path(); if let Some(file_name) = path.file_name().and_then(|name| name.to_str()) { if file_name.starts_with("cargo-dinghy-") && (path.is_file() && path .metadata() .map(|m| m.permissions().mode() & 0o111 != 0) .unwrap_or(false)) { binaries.push(file_name.to_string()); } } } } } } binaries.sort(); // ensure a deterministic order binaries } ================================================ FILE: dinghy-lib/src/project.rs ================================================ use crate::config::Configuration; use crate::utils::copy_and_sync_file; use crate::Platform; use crate::Result; use crate::Runnable; use anyhow::anyhow; use anyhow::Context; use cargo_metadata::Metadata; use fs_err as fs; use ignore::WalkBuilder; use log::{debug, trace}; use std::fs::File; use std::io::prelude::*; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; #[derive(Debug)] pub struct Project { pub conf: Arc, pub metadata: Metadata, } impl Project { pub fn new(conf: &Arc, metadata: Metadata) -> Project { Project { conf: Arc::clone(conf), metadata, } } pub fn project_dir(&self) -> Result { Ok(self.metadata.workspace_root.clone().into_std_path_buf()) } pub fn overlay_work_dir(&self, platform: &dyn Platform) -> Result { Ok(self .target_dir(platform.rustc_triple())? .join(platform.rustc_triple())) } pub fn target_dir(&self, triple: &str) -> Result { Ok(self .metadata .target_directory .clone() .into_std_path_buf() .join(triple)) } pub fn link_test_data(&self, runnable: &Runnable) -> Result { let test_data_path = runnable .exe .parent() .and_then(|it| it.parent()) .map(|it| it.join("dinghy")) .map(|it| it.join(runnable.exe.file_name().unwrap())) .map(|it| it.join("test_data")) .unwrap(); fs::create_dir_all(&test_data_path)?; let test_data_cfg_path = test_data_path.join("test_data.cfg"); let mut test_data_cfg = File::create(&test_data_cfg_path)?; debug!("Generating {}", test_data_cfg_path.display()); for td in self.conf.test_data.iter() { let target_path = td .base .parent() .unwrap_or(&PathBuf::from("/")) .join(&td.source); let target_path = target_path .to_str() .ok_or_else(|| anyhow!("Invalid UTF-8 path {}", target_path.display()))?; test_data_cfg.write_all(td.id.as_bytes())?; test_data_cfg.write_all(b":")?; test_data_cfg.write_all(target_path.as_bytes())?; test_data_cfg.write_all(b"\n")?; } Ok(test_data_path) } pub fn copy_test_data>(&self, app_path: T) -> Result<()> { let app_path = app_path.as_ref(); let test_data_path = app_path.join("test_data"); fs::create_dir_all(&test_data_path)?; for td in self.conf.test_data.iter() { let file = td .base .parent() .unwrap_or(&PathBuf::from("/")) .join(&td.source); if Path::new(&file).exists() { let metadata = file.metadata()?; let dst = test_data_path.join(&td.id); if metadata.is_dir() { rec_copy(file, dst, td.copy_git_ignored)?; } else { copy_and_sync_file(file, dst)?; } } else { log::warn!( "configuration required test_data `{:?}` but it could not be found", td ); } } Ok(()) } } pub fn rec_copy, P2: AsRef>( src: P1, dst: P2, copy_ignored_test_data: bool, ) -> Result<()> { let empty: &[&str] = &[]; rec_copy_excl(src, dst, copy_ignored_test_data, empty) } pub fn rec_copy_excl, P2: AsRef, P3: AsRef + ::std::fmt::Debug>( src: P1, dst: P2, copy_ignored_test_data: bool, more_exclude: &[P3], ) -> Result<()> { let src = src.as_ref(); let dst = dst.as_ref(); let ignore_file = src.join(".dinghyignore"); debug!( "Copying recursively from {} to {} excluding {:?}", src.display(), dst.display(), more_exclude ); let mut walker = WalkBuilder::new(src); walker.follow_links(true); walker.git_ignore(!copy_ignored_test_data); walker.add_ignore(ignore_file); for entry in walker.build() { let entry = entry?; let metadata = entry.metadata()?; if more_exclude.iter().any(|ex| entry.path().starts_with(ex)) { debug!("Exclude {:?}", entry.path()); continue; } trace!( "Processing entry {:?} is_dir:{:?}", entry.path(), metadata.is_dir() ); let path = entry.path().strip_prefix(src)?; // Check if root path is a file or a directory let target = if path.parent().is_none() && metadata.is_file() { fs::create_dir_all( &dst.parent() .ok_or_else(|| anyhow!("Invalid file {}", dst.display()))?, )?; dst.to_path_buf() } else { dst.join(path) }; if metadata.is_dir() { if target.exists() && target.is_file() { fs::remove_file(&target)?; } trace!("Creating directory {}", target.display()); fs::create_dir_all(&target)?; } else if metadata.is_file() { if target.exists() && !target.is_file() { trace!("Remove 2 {:?}", target); fs::remove_dir_all(&target)?; } if !target.exists() || target.metadata()?.len() != entry.metadata()?.len() || target.metadata()?.modified()? < entry.metadata()?.modified()? { if target.exists() && target.metadata()?.permissions().readonly() { fs::remove_dir_all(&target)?; } trace!("Copying {} to {}", entry.path().display(), target.display()); copy_and_sync_file(entry.path(), &target) .with_context(|| format!("Syncing {entry:?} and {target:?}"))?; } else { trace!("{} is already up-to-date", target.display()); } } else { debug!("ignored {:?} ({:?})", path, metadata); } } trace!( "Copied recursively from {} to {} excluding {:?}", src.display(), dst.display(), more_exclude ); Ok(()) } ================================================ FILE: dinghy-lib/src/script/device.rs ================================================ use crate::config::ScriptDeviceConfiguration; use crate::utils::LogCommandExt; use crate::*; use anyhow::bail; use std::{fmt, fs, process}; #[derive(Debug, Clone)] pub struct ScriptDevice { pub id: String, pub conf: ScriptDeviceConfiguration, } impl ScriptDevice { fn command(&self, _build: &Build) -> Result { if fs::metadata(&self.conf.path).is_err() { bail!("Can not read {:?} for {}.", self.conf.path, self.id); } let mut cmd = process::Command::new(&self.conf.path); cmd.env("DINGHY_TEST_DATA", &*self.id); cmd.env("DINGHY_DEVICE", &*self.id); if let Some(ref pf) = self.conf.platform { cmd.env("DINGHY_PLATFORM", &*pf); } Ok(cmd) } } impl Device for ScriptDevice { fn clean_app(&self, _build_bundle: &BuildBundle) -> Result<()> { Ok(()) } fn debug_app( &self, _project: &Project, _build: &Build, _args: &[&str], _envs: &[&str], ) -> Result { unimplemented!() } fn id(&self) -> &str { &self.id } fn name(&self) -> &str { &self.id } fn run_app( &self, project: &Project, build: &Build, args: &[&str], envs: &[&str], ) -> Result { let root_dir = build.target_path.join("dinghy"); let bundle_path = &build.runnable.source; log::trace!("About to start runner script..."); let test_data_path = project.link_test_data(&build.runnable)?; let status = self .command(build)? .arg(&build.runnable.exe) .current_dir(&build.runnable.source) .env("DINGHY_TEST_DATA_PATH", test_data_path) .args(args) .envs( envs.iter() .map(|kv| { Ok(( kv.split("=") .nth(0) .ok_or_else(|| anyhow!("Wrong env spec"))?, kv.split("=") .nth(1) .ok_or_else(|| anyhow!("Wrong env spec"))?, )) }) .collect::>>()?, ) .log_invocation(1) .status()?; if !status.success() { bail!("Test failed") } Ok(BuildBundle { id: build.runnable.id.clone(), bundle_dir: bundle_path.to_path_buf(), bundle_exe: build.runnable.exe.to_path_buf(), lib_dir: build.target_path.clone(), root_dir: root_dir.clone(), app_id: None, }) } } impl DeviceCompatibility for ScriptDevice { fn is_compatible_with_regular_platform(&self, platform: &RegularPlatform) -> bool { self.conf .platform .as_ref() .map_or(false, |it| *it == platform.id) } } impl Display for ScriptDevice { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { write!(fmt, "{}", self.id) } } ================================================ FILE: dinghy-lib/src/script/mod.rs ================================================ use crate::{Configuration, Device, Platform, PlatformManager, Result}; use std::sync; mod device; pub use self::device::ScriptDevice; pub struct ScriptDeviceManager { conf: sync::Arc, } impl ScriptDeviceManager { pub fn probe(conf: sync::Arc) -> Option { Some(ScriptDeviceManager { conf }) } } impl PlatformManager for ScriptDeviceManager { fn devices(&self) -> Result>> { Ok(self .conf .script_devices .iter() .map(|(k, conf)| { Box::new(ScriptDevice { id: k.clone(), conf: conf.clone(), }) as _ }) .collect()) } fn platforms(&self) -> Result>> { Ok(vec![]) } } ================================================ FILE: dinghy-lib/src/ssh/device.rs ================================================ use crate::config::SshDeviceConfiguration; use crate::device::make_remote_app; use crate::errors::*; use crate::host::HostPlatform; use crate::platform::regular_platform::RegularPlatform; use crate::project::Project; use crate::utils::{get_current_verbosity, path_to_str, user_facing_log, LogCommandExt}; use crate::Build; use crate::BuildBundle; use crate::Device; use crate::DeviceCompatibility; use std::fmt; use std::fmt::Formatter; use std::fmt::{Debug, Display}; use std::io::IsTerminal; use std::path::Path; use std::path::PathBuf; use std::process::Command; #[derive(Clone)] pub struct SshDevice { pub id: String, pub conf: SshDeviceConfiguration, } impl SshDevice { fn install_app(&self, project: &Project, build: &Build) -> Result<(BuildBundle, BuildBundle)> { user_facing_log( "Installing", &format!("{} to {}", build.runnable.id, self.id), 0, ); log::debug!("make_remote_app {}", build.runnable.id); let build_bundle = make_remote_app(project, build)?; log::trace!("make_remote_app {} done", build.runnable.id); let remote_bundle = self.to_remote_bundle(&build_bundle)?; log::trace!("Create remote dir: {:?}", remote_bundle.bundle_dir); let _ = self .ssh_command()? .arg("mkdir") .arg("-p") .arg(&remote_bundle.bundle_dir) .log_invocation(2) .status(); log::info!("Install {} to {}", build.runnable.id, self.id); self.sync(&build_bundle.bundle_dir, &remote_bundle.bundle_dir)?; self.sync(&build_bundle.lib_dir, &remote_bundle.lib_dir)?; Ok((build_bundle, remote_bundle)) } fn ssh_command(&self) -> Result { let mut command = Command::new("ssh"); if let Some(port) = self.conf.port { command.arg("-p").arg(&format!("{}", port)); } if std::io::stdout().is_terminal() { command.arg("-t").arg("-o").arg("LogLevel=QUIET"); } command.arg(format!("{}@{}", self.conf.username, self.conf.hostname)); Ok(command) } fn sync_rsync(&self) -> Result { match &self.conf.install_adhoc_rsync_local_path { Some(rsync) => { let rsync_path = PathBuf::from(self.conf.path.clone().unwrap_or("/tmp".into())) .join("dinghy") .join("rsync"); let rsync_path = rsync_path .to_str() .ok_or_else(|| anyhow!("Could not format rsync remote path"))?; if self .ssh_command()? .arg("[") .arg("-f") .arg(rsync_path) .arg("]") .log_invocation(2) .status()? .success() { log::debug!("ad-hoc rsync already present on device, skipping copy") } else { let mut command = Command::new("scp"); if let Some(true) = self.conf.use_legacy_scp_protocol_for_adhoc_rsync_copy { command.arg("-O"); } command.arg("-q"); if let Some(port) = self.conf.port { command.arg("-P").arg(&format!("{}", port)); } command.arg(format!("{}", rsync)); command.arg(format!( "{}@{}:{}", self.conf.username, self.conf.hostname, rsync_path )); log::debug!("Running {:?}", command); if !command.log_invocation(3).status()?.success() { bail!("Error copying rsync binary ({:?})", command) } } Ok(rsync_path.to_string()) } None => Ok("/usr/bin/rsync".to_string()), } } fn sync, TP: AsRef>(&self, from_path: FP, to_path: TP) -> Result<()> { let rsync = self.sync_rsync(); let rsync = match rsync { Ok(rsync_path) => rsync_path, Err(error) => bail!("Problem with rsync on the target: {:?}", error), }; let mut command = Command::new("rsync"); command.arg(&format!("--rsync-path={}", rsync)); command.arg("-a").arg("-v"); if let Some(port) = self.conf.port { command.arg("-e").arg(&*format!("ssh -p {}", port)); }; if !log::log_enabled!(::log::Level::Debug) { command.stdout(::std::process::Stdio::null()); command.stderr(::std::process::Stdio::null()); } command .arg(&format!("{}/", path_to_str(&from_path.as_ref())?)) .arg(&format!( "{}@{}:{}/", self.conf.username, self.conf.hostname, path_to_str(&to_path.as_ref())? )); log::debug!("Running {:?}", command); if !command .log_invocation(1) .status() .with_context(|| format!("failed to run '{:?}'", command))? .success() { bail!("Error syncing ssh directory ({:?})", command) } else { Ok(()) } } fn to_remote_bundle(&self, build_bundle: &BuildBundle) -> Result { let remote_prefix = PathBuf::from(self.conf.path.clone().unwrap_or("/tmp".into())).join("dinghy"); build_bundle.replace_prefix_with(remote_prefix) } } impl DeviceCompatibility for SshDevice { fn is_compatible_with_regular_platform(&self, platform: &RegularPlatform) -> bool { self.conf .platform .as_ref() .map_or(false, |it| *it == platform.id) } fn is_compatible_with_host_platform(&self, platform: &HostPlatform) -> bool { self.conf .platform .as_ref() .map_or(true, |it| *it == platform.id) } } impl Device for SshDevice { fn clean_app(&self, build_bundle: &BuildBundle) -> Result<()> { let status = self .ssh_command()? .arg(&format!( "rm -rf {}", path_to_str(&build_bundle.bundle_exe)? )) .log_invocation(1) .status()?; if !status.success() { bail!("test fail.") } Ok(()) } fn debug_app( &self, _project: &Project, _build: &Build, _args: &[&str], _envs: &[&str], ) -> Result { unimplemented!() } fn id(&self) -> &str { &self.id } fn name(&self) -> &str { &self.id } fn run_app( &self, project: &Project, build: &Build, args: &[&str], envs: &[&str], ) -> Result { let remote_shell_vars_as_context = |a: &str| -> Option> { self.conf.remote_shell_vars.get(a).map(|s| s.into()) }; let args: Vec = args .iter() .map(|&a| { shellexpand::full_with_context_no_errors( a, || remote_shell_vars_as_context("HOME"), remote_shell_vars_as_context, ) }) .map(|a| ::shell_escape::escape(a).to_string()) .collect(); log::info!("Install {:?}", build.runnable.id); let (build_bundle, remote_bundle) = self.install_app(&project, &build)?; log::debug!("Installed {:?}", build.runnable.id); let command = format!( "cd '{}' ; RUST_BACKTRACE=1 {} DINGHY=1 LD_LIBRARY_PATH=\"{}:$LD_LIBRARY_PATH\" {} {}", path_to_str(&remote_bundle.bundle_dir)?, envs.join(" "), path_to_str(&remote_bundle.lib_dir)?, path_to_str(&remote_bundle.bundle_exe)?, args.join(" ") ); log::trace!("Ssh command: {}", command); log::info!("Run {} on {}", build.runnable.id, self.id,); if get_current_verbosity() < 1 { // we log the full command for verbosity > 1, just log a short message when the user // didn't ask for verbose output user_facing_log( "Running", &format!("{} on {}", build.runnable.id, self.id), 0, ); } let status = self .ssh_command()? .arg(&command) .log_invocation(1) .status()?; if !status.success() { bail!("Failed") } Ok(build_bundle) } } impl Debug for SshDevice { fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { Ok(fmt.write_str(format!("Ssh {{ \"id\": \"{}\", \"hostname\": \"{}\", \"username\": \"{}\", \"port\": \"{}\" }}", self.id, self.conf.hostname, self.conf.username, self.conf.port.as_ref().map_or("none".to_string(), |it| it.to_string())).as_str())?) } } impl Display for SshDevice { fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { write!(fmt, "{} ({})", self.id, self.conf.hostname) } } ================================================ FILE: dinghy-lib/src/ssh/mod.rs ================================================ mod device; use crate::{Configuration, Device, Platform, PlatformManager, Result}; use std::sync; pub use self::device::SshDevice; pub struct SshDeviceManager { conf: sync::Arc, } impl SshDeviceManager { pub fn probe(conf: sync::Arc) -> Option { Some(SshDeviceManager { conf }) } } impl PlatformManager for SshDeviceManager { fn devices(&self) -> Result>> { Ok(self .conf .ssh_devices .iter() .map(|(k, conf)| { Box::new(SshDevice { id: k.clone(), conf: conf.clone(), }) as _ }) .collect()) } fn platforms(&self) -> Result>> { Ok(vec![]) } } ================================================ FILE: dinghy-lib/src/toolchain.rs ================================================ use crate::errors::*; use crate::SetupArgs; use dinghy_build::build_env::append_path_to_env; use dinghy_build::build_env::append_path_to_target_env; use dinghy_build::build_env::envify; use dinghy_build::build_env::set_env; use dinghy_build::build_env::set_target_env; use itertools::Itertools; use std::io::Write; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use std::{fs, path}; use walkdir::WalkDir; #[cfg(not(target_os = "windows"))] static GLOB_ARGS: &str = r#""$@""#; #[cfg(target_os = "windows")] static GLOB_ARGS: &str = r#"%*"#; #[derive(Clone, Debug)] pub struct Toolchain { pub rustc_triple: String, } impl Toolchain { pub fn setup_tool(&self, var: &str, exe: &str) -> Result<()> { set_env(format!("TARGET_{}", var), exe); set_env(format!("{}_{}", var, self.rustc_triple), exe); Ok(()) } pub fn setup_cc(&self, _id: &str, compiler_command: &str) -> Result<()> { set_env("TARGET_CC", compiler_command); set_env(format!("CC_{}", self.rustc_triple), compiler_command); Ok(()) } pub fn setup_linker>( &self, id: &str, linker_command: &str, workspace_root: P, ) -> Result<()> { let shim = create_shim( workspace_root, &self.rustc_triple, id, "linker", format!("{} {}", linker_command, GLOB_ARGS).as_str(), )?; set_env( format!("CARGO_TARGET_{}_LINKER", envify(self.rustc_triple.as_str())).as_str(), shim, ); Ok(()) } pub fn setup_pkg_config(&self) -> Result<()> { set_env("PKG_CONFIG_ALLOW_CROSS", "1"); set_target_env("PKG_CONFIG_LIBPATH", Some(&self.rustc_triple), ""); Ok(()) } pub fn setup_runner(&self, platform_id: &str, setup_args: &SetupArgs) -> Result<()> { set_env( format!("CARGO_TARGET_{}_RUNNER", envify(self.rustc_triple.as_str())).as_str(), setup_args.get_runner_command(platform_id), ); Ok(()) } pub fn setup_target(&self) -> Result<()> { set_env("CARGO_BUILD_TARGET", &self.rustc_triple); Ok(()) } } #[derive(Clone, Debug)] pub struct ToolchainConfig { pub bin_dir: PathBuf, pub root: PathBuf, pub rustc_triple: String, pub sysroot: Option, pub cc: String, pub cxx: String, pub binutils_prefix: String, pub cc_prefix: String, } impl ToolchainConfig { pub fn cc_executable(&self, name_without_triple: &str) -> String { self.bin_dir .join(format!("{}-{}", self.cc_prefix, name_without_triple)) .to_string_lossy() .to_string() } pub fn binutils_executable(&self, name_without_triple: &str) -> String { self.bin_dir .join(format!("{}-{}", self.binutils_prefix, name_without_triple)) .to_string_lossy() .to_string() } pub fn naked_executable(&self, name: &str) -> String { self.bin_dir.join(name).to_string_lossy().to_string() } pub fn setup_pkg_config(&self) -> Result<()> { self.as_toolchain().setup_pkg_config()?; if self.root.parent().is_some() { append_path_to_target_env( "PKG_CONFIG_LIBDIR", Some(&self.rustc_triple), WalkDir::new(self.root.to_string_lossy().as_ref()) .into_iter() .filter_map(|e| e.ok()) // Ignore unreadable files, maybe could warn... .filter(|e| e.file_name() == "pkgconfig" && e.file_type().is_dir()) .map(|e| e.path().to_string_lossy().into_owned()) .join(":"), ); } if let Some(sr) = &self.sysroot { set_target_env("PKG_CONFIG_SYSROOT_DIR", Some(&self.rustc_triple), &sr); } Ok(()) } pub fn setup_sysroot(&self) { if let Some(sr) = &self.sysroot { set_env("TARGET_SYSROOT", sr); } } pub fn setup_tool(&self, var: &str, command: &str) -> Result<()> { self.as_toolchain().setup_tool(var, command) } pub fn setup_cc(&self, id: &str, compiler_command: &str) -> Result<()> { self.as_toolchain().setup_cc(id, compiler_command) } pub fn generate_linker_command(&self, setup_args: &SetupArgs) -> String { let mut linker_cmd = self.cc_executable(&*self.cc); linker_cmd.push_str(" "); if setup_args.verbosity > 0 { linker_cmd.push_str("-Wl,--verbose -v") } if let Some(sr) = &self.sysroot { linker_cmd.push_str(&format!(" --sysroot {}", sr.display())); } for forced_overlay in &setup_args.forced_overlays { linker_cmd.push_str(" -l"); linker_cmd.push_str(&forced_overlay); // TODO Add -L } linker_cmd } pub fn setup_linker>( &self, id: &str, linker_command: &str, workspace_root: P, ) -> Result<()> { self.as_toolchain() .setup_linker(id, linker_command, workspace_root) } pub fn setup_runner(&self, platform_id: &str, setup_args: &SetupArgs) -> Result<()> { self.as_toolchain().setup_runner(platform_id, setup_args) } pub fn setup_target(&self) -> Result<()> { self.as_toolchain().setup_target() } pub fn shim_executables>( &self, id: &str, workspace_root: P, ) -> Result<()> { let workspace_root = workspace_root.as_ref(); let shims_path = workspace_root .join("target") .join(&self.rustc_triple) .join(id); for exe in self.bin_dir.read_dir()? { let exe = exe?; let exe_file_name = exe.file_name(); let exe_path = exe.path(); let exe_path = exe_path.to_string_lossy(); let rustified_exe = &exe_file_name .to_string_lossy() .replace(self.binutils_prefix.as_str(), self.rustc_triple.as_str()) .replace(self.cc_prefix.as_str(), self.rustc_triple.as_str()); log::trace!("Shim {} -> {}", exe_path, rustified_exe); create_shim( workspace_root, self.rustc_triple.as_str(), id, rustified_exe, &format!("{} {}", exe_path, GLOB_ARGS), )?; } append_path_to_env("PATH", shims_path.to_string_lossy().as_ref()); Ok(()) } fn as_toolchain(&self) -> Toolchain { Toolchain { rustc_triple: self.rustc_triple.clone(), } } } fn create_shim>( root: P, rustc_triple: &str, id: &str, name: &str, shell: &str, ) -> Result { let target_shim_path = root.as_ref().join("target").join(rustc_triple).join(id); fs::create_dir_all(&target_shim_path)?; let mut shim = target_shim_path.join(name); if cfg!(target_os = "windows") { shim.set_extension("bat"); }; let mut linker_shim = fs::File::create(&shim)?; if !cfg!(target_os = "windows") { writeln!(linker_shim, "#!/bin/sh")?; } linker_shim.write_all(shell.as_bytes())?; writeln!(linker_shim, "\n")?; #[cfg(unix)] fs::set_permissions(&shim, PermissionsExt::from_mode(0o777))?; Ok(shim) } ================================================ FILE: dinghy-lib/src/utils.rs ================================================ use crate::errors::Result; use anyhow::{anyhow, bail}; use filetime::set_file_times; use filetime::FileTime; use fs_err as fs; use lazy_static::lazy_static; use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::sync::atomic::{AtomicI8, Ordering}; pub fn copy_and_sync_file, Q: AsRef>(from: P, to: Q) -> Result<()> { let from = &from.as_ref(); let to = &to.as_ref(); if !from.exists() { bail!("Source {from:?} is missing") } if !to.parent().unwrap().exists() { bail!("Target directory is missing") } // Make target file writeable if it is read-only. if to.exists() { let mut permissions = fs::metadata(&to)?.permissions(); if permissions.readonly() { permissions.set_readonly(false); fs::set_permissions(&to, permissions)?; } } log::trace!("copy {:?} to {:?}", from, to); fs::copy(&from, &to)?; // Keep filetime to avoid useless sync on some devices (e.g. Android). let from_metadata = from.metadata()?; let atime = FileTime::from_last_access_time(&from_metadata); let mtime = FileTime::from_last_modification_time(&from_metadata); set_file_times(&to, atime, mtime)?; Ok(()) } pub fn path_to_str<'a>(path: &'a Path) -> Result<&'a str> { Ok(path .to_str() .ok_or_else(|| anyhow!("Path is invalid '{}'", path.display()))?) } pub fn normalize_path(path: &Path) -> PathBuf { PathBuf::from(path.to_string_lossy().replace("\\", "/")) } pub fn contains_file_with_ext(dir_path: &Path, ext: &str) -> bool { if !dir_path.is_dir() { return false; }; if let Ok(path) = dir_path.read_dir() { for file in path { if let Ok(file) = file { if file.file_name().to_string_lossy().ends_with(ext) { return true; } } } } false } pub fn destructure_path>(path: P) -> Option<(PathBuf, String)> { let path = path.as_ref(); path.file_name() .and_then(|it| it.to_str()) .map(|name| (path.to_path_buf(), name.to_string())) } pub fn file_has_ext(file_path: &Path, ext: &str) -> bool { file_path.is_file() && file_path .file_name() .and_then(|it| it.to_str()) .map(|it| it.ends_with(ext)) .unwrap_or(false) } pub fn is_library(file_path: &Path) -> bool { file_path.is_file() && file_path .file_name() .and_then(|it| it.to_str()) .map(|it| { it.ends_with(".so") || it.contains(".so.") || it.ends_with(".dylib") || it.ends_with(".a") }) .unwrap_or(false) } pub fn lib_name_from(file_path: &Path) -> Result { let file_name = file_path .file_name() .and_then(|it| it.to_str()) .ok_or_else(|| { anyhow!( "'{}' doesn't point to a valid lib name", file_path.display() ) })?; let (start_index, end_index) = file_name .find(".so") .map(|end_index| (if file_name.starts_with("lib") { 3 } else { 0 }, end_index)) .unwrap_or((0, file_name.len())); if start_index == end_index { bail!( "'{}' doesn't point to a valid lib name", file_path.display() ); } else { Ok(file_name[start_index..end_index].to_string()) } } pub fn file_name_as_str(file_path: &Path) -> Result<&str> { Ok(file_path .file_name() .and_then(|it| it.to_str()) .ok_or_else(|| anyhow!("'{}' is not a valid file name", file_path.display()))?) } lazy_static! { static ref CURRENT_VERBOSITY: AtomicI8 = AtomicI8::new(0); } pub fn set_current_verbosity(verbosity: i8) { CURRENT_VERBOSITY.store(verbosity, Ordering::SeqCst) } pub fn get_current_verbosity() -> i8 { CURRENT_VERBOSITY.load(Ordering::SeqCst) } pub fn user_facing_log(category: &str, message: &str, verbosity: i8) { use colored::Colorize; if verbosity <= get_current_verbosity() { eprintln!("{:>12} {}", category.blue().bold(), message) } } pub trait LogCommandExt { fn log_invocation(&mut self, verbosity: i8) -> &mut Self; } impl LogCommandExt for Command { fn log_invocation(&mut self, verbosity: i8) -> &mut Self { user_facing_log( "Running", &format!( "{}{:?}", if verbosity + 1 < get_current_verbosity() { self.get_envs() .map(|(var_name, var_value)| { format!( "{}={:?} ", var_name.to_str().unwrap(), var_value.and_then(|it| it.to_str()).unwrap_or("") ) }) .fold(String::new(), |mut result, env| { result.push_str(&env); result }) } else { String::new() }, self ), verbosity, ); self } } ================================================ FILE: dinghy-test/Cargo.toml ================================================ [package] name = "dinghy-test" version = "0.8.5-pre" authors = ["Mathieu Poumeyrol "] license = "MIT/Apache-2.0" description = "Cross-compilation made easier - see main crate cargo-dinghy" homepage = "https://medium.com/snips-ai/dinghy-painless-rust-tests-and-benches-on-ios-and-android-c9f94f81d305#.c2sx7two8" repository = "https://github.com/sonos/dinghy" keywords = [ "tests", "mobile", "ios", "android", "cargo" ] categories = [ "development-tools::cargo-plugins", "development-tools::testing" , "development-tools::profiling" ] readme = "../README.md" edition = "2018" ================================================ FILE: dinghy-test/src/lib.rs ================================================ use std::env; use std::fs::File; use std::io::prelude::*; use std::path::PathBuf; pub fn test_project_path() -> PathBuf { if cfg!(any( target_os = "ios", target_os = "watchos", target_os = "tvos", target_os = "android" )) || env::var("DINGHY").is_ok() { let current_exe = env::current_exe().expect("Current exe path not accessible"); current_exe .parent() .expect(&format!( "Current exe path is invalid {}", current_exe.display() )) .into() } else { PathBuf::from(".") } } pub fn test_file_path(test_data_id: &str) -> PathBuf { try_test_file_path(test_data_id).expect(&format!("Couldn't find test data {}", test_data_id)) } pub fn try_test_file_path(test_data_id: &str) -> Option { let current_exe = env::current_exe().expect("Current exe path not accessible"); if cfg!(any( target_os = "ios", target_os = "watchos", target_os = "tvos", target_os = "android" )) || env::var("DINGHY").is_ok() { current_exe .parent() .map(|it| it.join("test_data")) .map(|it| it.join(test_data_id)) } else { let test_data_path = current_exe .parent() .and_then(|it| it.parent()) .map(|it| it.join("dinghy")) .map(|it| it.join(current_exe.file_name().unwrap())) .map(|it| it.join("test_data")); let test_data_path = match test_data_path { None => return None, Some(test_data_cfg_path) => test_data_cfg_path, }; let test_data_cfg_path = test_data_path.join("test_data.cfg"); let mut contents = String::new(); let test_data_cfg = File::open(&test_data_cfg_path).and_then(|mut f| f.read_to_string(&mut contents)); if let Err(_) = test_data_cfg { return None; } contents .lines() .map(|line| line.split(":")) .map(|mut line| (line.next(), line.next())) .find(|&(id, _)| id.map(|it| it == test_data_id).unwrap_or(false)) .and_then(|(_, path)| path) .map(PathBuf::from) } } ================================================ FILE: docs/android.md ================================================ ## Getting started - Android phone ### Dinghy setup Assuming [rustup](http://rustup.rs) is already installed... ``` cargo install cargo-dinghy # If it's already installed, add '--force' cargo install cargo-dinghy --force ``` ### ADB `adb` must be in your $PATH and your phone must have debugging enabled. enabled. See [adb doc](https://developer.android.com/studio/command-line/adb.html) . `adb devices -l` must show your phone like that when you connect it. ``` % adb devices -l List of devices attached 3100b123456789 device usb:341966848X product:a3xeltexx model:SM_A310F device:a3xelte ``` Now dinghy should also "see" your phone: ``` % cargo dinghy all-devices List of available devices for all platforms: Host { } Android { "id": "3100b123456789", "supported_targets": ["armv7-linux-androideabi", "arm-linux-androideabi"] } ``` ### Android NDK Starting NDK version r19, it is possible to use the NDK directly without building a standalone Android toolchain. It is the new recommended way to build Android binaries using Dinghy. First, download the [Android NDK](https://developer.android.com/ndk/downloads) or install the Android SDK. If you downloaded the NDK, make sure to set the environment variable `ANDROID_NDK_HOME` to point to the extracted NDK folder. If you installed the SDK, Dinghy should detect it automatically. If you did everything correctly, Dinghy should be able to recognize a large quantities of platforms. The following is an example of what you should see : ``` % cargo dinghy all-platforms * auto-android-aarch64 (aarch64-linux-android) * auto-android-aarch64-api21 (aarch64-linux-android) [...] * auto-android-aarch64-api28 (aarch64-linux-android) * auto-android-aarch64-latest (aarch64-linux-android) * auto-android-aarch64-min (aarch64-linux-android) * auto-android-armv7 (armv7-linux-androideabi) * auto-android-armv7-api16 (armv7-linux-androideabi) [...] * auto-android-armv7-api28 (armv7-linux-androideabi) * auto-android-armv7-latest (armv7-linux-androideabi) * auto-android-armv7-min (armv7-linux-androideabi) * auto-android-i686 (i686-linux-android) * auto-android-i686-api16 (i686-linux-android) [...] * auto-android-i686-api28 (i686-linux-android) * auto-android-i686-latest (i686-linux-android) * auto-android-i686-min (i686-linux-android) * auto-android-x86_64 (x86_64-linux-android) * auto-android-x86_64-api21 (x86_64-linux-android) [...] * auto-android-x86_64-api28 (x86_64-linux-android) * auto-android-x86_64-latest (x86_64-linux-android) * auto-android-x86_64-min (x86_64-linux-android) * host ``` As you can notice, there is a huge amount of new platforms that have appeared as `auto-android-[ARCHITECTURE]`. You can use those new platforms to build your binaries using the architecture you want but also the Android API level you want to use. You can explicitly build by providing the API level you want or you can ask either the earliest API level available or the latest API level using the aliases `min` and `latest`. You can also use the default API level which will default for the API level 21, which is the first common API level with 64 bits based architecture and 32 bits based architecture. If you get all the platforms, your NDK is set up. To finish your setup, you should [install the appropriate Rust target](#rust-target). ### Android standalone toolchain Before Android NDK version r19, Dinghy couldn't use the NDK directly, so you had to set up a standalone toolchain for your phone architecture: dinghy gave you the possible ones, (`arm` or `armv7` here). It is still possible to use that procedure, but the recommended procedure is now to use the Android NDK directly. Here are the steps to achieve: * [download and install Android NDK](https://developer.android.com/ndk/downloads/index.html) (please note that only r18 and previous are working to generate a standalone toolchain) * [build a stand-alone Android toolchain](https://developer.android.com/ndk/guides/standalone_toolchain.html#creating_the_toolchain) matching your phone architecture Note that `arm64` and `aarch64` are two names to the same architecture. After you generated your toolchain, you'll need to declare the platform in `~/.dinghy.toml`. It links everything together: the rustc target and your android toolchain. ``` [platforms.android-armv7] rustc_triple="armv7-linux-androideabi" toolchain="" ``` ### Rust target After you set up the NDK or the standalone toolchain, you may need to ask rustup to install the relevant target. ``` rustup target install armv7-linux-androideabi ``` ARMv7 is the most likely architecture for Android devices. However, maybe yours is one of these (two last are very unlikely): ``` rustup target install arm-linux-androideabi rustup target install aarch64-linux-android rustup target install i686-linux-android rustup target install x86_64-linux-android ``` ### Try it Let's try it with the Dinghy demo project. The project tests with "pass" in the name is supposed to pass, the one with fail should break. ``` % git clone https://github.com/sonos/dinghy % cd dinghy/test-ws [...] # these ones should pass % cargo dinghy -d android test pass [...] # this one shall not pass % cargo dinghy -d android test fail [...] ``` That's it! Enjoy! ================================================ FILE: docs/dinghy-build.md ================================================ ## Build script helpers [WIP] Dinghy also provides a dinghy-helper crate to help with the writing of build scripts that performs cross-compilation, and more specifically: - CommandExt: a std::process::Command extension that can: - Setup pkg-config environment variables for a subprocess (`PKG_CONFIG_LIBDIR`, ... e.g. when running Automake `./configure`) - Setup toolchain environment variables for a subprocess (`TARGET_CC`, ...) - A few other useful methods (e.g. `configure_prefix()` to set the prefix to rust output dir when running Automake `./configure`) - BindGenBuilderExt: a bindgen::Builder extension to help writing C to rust bindings that supports cross compilation properly (see `new_bindgen_with_cross_compilation_support()`). *This is still a WIP. Beware of possible breaking changes* ================================================ FILE: docs/files.md ================================================ ## Sending project files to the devices Some tests are relying on the presence of files at relative paths to be able to proceed. But we can not always control where we will be executing from (we can not always do `cd someplace` before running the tests). So, the tests are "bundled" in the following way: * root dinghy test directory * test_executable * recursive copy of the not-ignorable files and directories from your projects * test_data contains configurable data to be sent to the device * some_file * some_dir Anything in .gitignore or .dinghyignore will not be bundled. To open your test file easily, you can use the dinghy-test crate in your tests which contains a helper function to access your project directory: ```rust #[cfg(test)] extern crate dinghy_test; #[cfg(test)] mod tests { #[test] fn my_test() { let my_file_path = dinghy_test::test_project_path().join("tests/data_1.txt"); // ... } } ``` ## Sending more files to the devices Now let's assume you have out-of-repository files to send over. You can do that by adding it in `.dinghy.toml` (you'll probably want this one in the project directory, or just above it if the data is shared among several cargo projects). ```toml [test_data] the_data = "../data-2017-02-05" conf_file = "/etc/some/file" ``` The keys are the name under which to look for files below "test_data" in the bundles, and the values are what to be copied (from your development workstation). By default anything in `.gitignore` or `.dinghyignore` is not copied, however if you need .gitignore'd files to be copied it can be excluded by adding `copy_git_ignored = true`: ```toml [test_data] the_data = { source = "../data-2017-02-05", copy_git_ignored = true } conf_file = "/etc/some/file" ``` Then you can use again the dinghy-test crate to access your specific test data directory: ```rust #[cfg(test)] extern crate dinghy_test; #[cfg(test)] mod tests { #[test] fn my_test() { let my_test_data_path = dinghy_test::test_file_path("the_data"); let my_test_file_path = dinghy_test::test_file_path("conf_file"); // ... } } ``` In order to accomodate tests running in "cargo native" and "dinghy mode" you may define and use a helper like that in your tests to find the test files: ```rust #[cfg(test)] fn test_resources() -> std::path::PathBuf { dinghy_test::try_test_file_path("test-resources").unwrap_or("../test-resources".into()) } ``` ================================================ FILE: docs/filter.md ================================================ ### Package filtering Rust workspace builds all contained packages by default. However, when a worspace project builds multiple platforms, some packages might not be buildable on all targets. To help with that situation, Dinghy has a filter option which allows benching, building or testing only the packages compatible with the current target platform. To make it works, modify the package toml to include some Dinghy metadata specifying the allowed rustc triples. For example, here we allow android only: ```toml [package.metadata.dinghy] allowed_rustc_triples = [ "armv7-linux-androideabi", "aarch64-linux-android", "i686-linux-android", "x86_64-linux-android" ] ``` Or specifying the rustc triples to ignore. For example, here we disallow ios: ```toml [package.metadata.dinghy] ignored_rustc_triples = ["aarch64-apple-ios", "armv7-apple-ios", "armv7s-apple-ios", "i386-apple-ios", "x86_64-apple-ios"] ``` ================================================ FILE: docs/ios.md ================================================ ## Getting started - iOS phone ### Dinghy setup Assuming [rustup](http://rustup.rs) is already installed... ``` cargo install cargo-dinghy ``` ### Additional iOS Requirements You will need: * XCode (the full thing, not restricted to the command line) * [`pymobiledevice3`](https://github.com/doronz88/pymobiledevice3) for deploying and running code on the devices. `python3 -m pip install -U pymobiledevice3` should do the trick. * optionnaly, for iOS devices running iOS version lower than 17, you need [ios-deploy](https://github.com/ios-control/ios-deploy): `brew install ios-deploy` ### iOS phone On iOS, things are made complicated by the fact that there is no way to run a naked executable on a device: you need to make it an app, and sign it before sending it to your phone. Setting up app signature requires some manipulations that we could not find a way to dispense with through XCode. The good news is, this is a constant time thing: *you'll have do it once in a while, but it will cover all your Rust projects*. Again, we don't need a paying account. ### Creating a signing id You may skip most of this section if you're already setup to ship code to your phone. * You'll need an Apple ID. Chances are you already have one, but you can an account there: https://appleid.apple.com/account . * Go to XCode > Preferences/Settings > Accounts, and make sure your Apple ID is listed, or add it (`+` bottom well-hidden bottom left). * View Details (bottom right this time) * Click the "Create" button in front of "iOS Development" in the top half of this windows. ### Creating a certificate * Plug-in your iPhone (or the device you want to run test on). * Fire up XCode, and /create a new XCode project/. * Pick /iOS/, /Single view application/. * Options box: * Make the bundle identifier `some.unique.domainame.Dinghy`. * So Product Name is `Dinghy`, and `Organization identifier` is something that will be unique and looks like a domain name in reverse. * Pick your team. * Leave the rest alone * Save it somewhere. * You should see the dreadful projects settings screen. The only thing that is relevant for us is the "Signing" bit. * If you see your Team there, and nothing red or yellows shows up, then you're OK. In some cases, you have to push the "repair" button to actually obtain a certificate from Apple. If you're using a Apple ID free account, you will need to come back once a week to refresh the certificate (aka "repair"). Paying account generate longer living certificates, so you need this less often. ### Trust the certificate on your phone On top of project window, make the target "Dinghy>Your Device", and run the project (play button). XCode may ask you to go to your phone settings and Trust the certificate. It's in the XCode > Preferences/Settings > General > [your dev account name]. It should then start your empty app on the phone. At this point, we're ready to roll, dinghy should detect XCode and the various toolchain on its own. ### Try it Let's try it with dinghy demo project. The project tests with "pass" in the name are supposed to pass, the one with fail should break. ``` % git clone https://github.com/sonos/dinghy % cd dinghy/test-ws [...] # these ones should pass % cargo dinghy -d iphone test pass [...] # this one shall not pass % cargo dinghy -d iphone test fail [...] ``` ### Simulator There's a [known bug with lldb and the ios simulator](https://bugs.llvm.org/show_bug.cgi?id=36580) as such, dinghy will use lldb to attach to the process on macOS to get the exit status from the simulator. On Catalina (and probably earlier), this means the user will be prompted for higher permissions. ### Debugging tips If you got lost somewhere, here are a few hints to help you make sense of what is happening. This is more or less what Dinghy use when fishing for your signing identity. #### `security find-identity -p codesigning` Shows you the codesigning identities available where you are. You should see one or more identities line, made of a long capitalize hex identifier, followed by a "name". The name is very structured: For iOS development , its starts with the string "iPhone Developer: ", followed by an email (for an Apple Id account) or the name of your team. Then comes the developer identifier in parenthesis. #### `security find-certificate -a -c "name" -p | openssl x509 -text` "name" is the identity name (the string between double quotes) from the command before. Shows you a certificate that makes the developer a part of a Team. The certificate is signed and issued by Apple (Issuer:), but the interesting part is the Subject line: CN= is the same developer name string, and OU= is the actual Team identifier. #### Look for provisioning certificates Last but not least, we need one more certificate that proves that you and your team has the right to deploy an App (identified by the bundle id we have chosen while creating the project) on one (or more) devices. These certificates are in `Library/MobileDevice/Provisioning\ Profiles`. To read them, you'll need to do ``` security cms -D -i ~/Library/MobileDevice/Provisioning\ Profiles\....mobileprovision ``` The more interesting keys here are `TeamIdentifier`, `ProvisionedDevices` which are relatively explicit, and the `Entitlements` dictionary. These entitlements specify what the certificate is valid for, that is, signing an app identified by a name. Dinghy will actually scan this directory to find one that it can use (this is where the app name being "Dinghy" plays a role). Phew. ================================================ FILE: docs/overlay.md ================================================ ### Overlays A toolchain might not contains all the required dependencies for your project. To help with situation, Dinghy offers overlays. #### Overlay configuration By default, Dinghy will look for overlays in the `dinghy/overlay/` directory next to your configuration file. Overlay directories can also be specified using the overlays section of your platform configuration: ```toml [platforms.android-arm64] overlays={ mydep={ path="/mypath" } } ``` #### Overlay directory An overlay is a directory which contains the required *.so*, *.h* and *.pc* files for a dependency. For example: ``` my-overlay |- libmylib.so |- bmylib.h |- bmylib.pc ``` An overlay is like an additional sysroot. So you can also create directories and subdirectories. For example, we use a tensorflow overlay on our android projects with the following structure: ``` android-arm64 |- overlay |- tensorflow |- usr |- include |- pkgconfig |- tensorflow.pc |- lib |- tensorflow.so ``` Dinghy looks for: - *.pc* files either in the overlay root or in all sub-directories named `pkgconfig`. - *.so* libraries in the overlay directory and all of its subdirectories. #### Overlay pkg-config Dinghy uses pkg-config to append dependencies during the compilation process (technically speaking using `PKG_CONFIG_LIBDIR`). By default, if no pkgconfig *.pc* file is found, Dinghy will generate one before the build. In such a case, the overlay directory itself is appended as include and linking path in the pkgconfig files along all the `.so` files founds in its root. For example: ``` prefix=/ Name: mylib Description: mylib Requires: Version: unspecified Libs: -L${prefix} -lmylib Cflags: -I${prefix} ``` Ideally, you should create a *.pc* file to make sure all compilation flags are set-up correctly. For example, our tensorflow overlay includes the following *.pc*: ``` prefix=/ exec_prefix=${prefix} libdir=${exec_prefix}/usr/lib includedir=${prefix}/usr/include Name: Tensorflow Description: Tensorflow for Android Requires: Version: 1.5 Cflags: -I${includedir} Libs: -L${libdir} -ltensorflow -lstdc++ -landroid -lz ``` Overlays are usually outside the toolchain sysroot. As a consequence, Dinghy must overrides the `prefix` pkg-config variable to provide a correct overlay path relative to the toolchain sysroot, despite being outside of it. Hence, when writing a *.pc* file, it's very *important* to: - Define a `prefix` variable that Dinghy can override - Consider that this `prefix` points to the root of the overlay directory #### Overlay runtime To make sure overlays are available at runtime, during benches, run or tests, Dinghy will copied all the `.so` files linked by the linker script during a build on the target device before running the appropriate executable. ================================================ FILE: docs/ssh.md ================================================ ## Getting started - Ssh device Ssh setup is useful if you want to target small-ish devices that can host an operating system but are too slow to iterate over Rust compilation/test cycles comfortably. Think... something like a Raspberry, a NAS, or an old x86 box in your garage. ### Dinghy setup Assuming [rustup](http://rustup.rs) is already installed... ``` cargo install cargo-dinghy # If it's already installed, add '--force' cargo install cargo-dinghy --force ``` ### Finding a toolchain This is the hard part. Rust or dinghy will not solve anything magically here. If you're lucky, your hardware vendor has provided you with one you can actually use. Or somebody on the internet did the hard work. Or else https://crosstool-ng.github.io/ will become your best friend. Dinghy will assume the toolchain looks relatively "regular". That is, it expects to find something that looks like a `sysroot`, a directory called bin with a compiler and binutils. Once you have this toolchain, that can compile and link a simple C helloworld to something running on your device, you're ready to start playing with rust and dinghy. ### Install Rust target ``` rustup target install arm-unknown-linux-gnueabi ``` ### Configure dinghy The minimum configuration to be added on `~/.dinghy.toml` should look like that. You need to define a platform by linking rustc target to your toolchain, and to link your device and account to this platform. We recommend that you add your ssh key to the account you want to link to, or the password prompts will drive you crazy. ``` [platforms.raspbian-stretch] rustc_triple="arm-unknown-linux-gnueabihf" toolchain="/path/to/a/toolchain/for/arm-unknown-linux-gnueabi" [ssh_devices] raspi = { hostname = "raspi.local", username="pi", platform="raspbian-stretch" } ``` ### Try it Let's try it with dinghy demo project. The project tests with "pass" in the name are supposed to pass, the one with fail should break. ``` % git clone https://github.com/sonos/dinghy % cd dinghy/test-ws [...] # these ones should pass % cargo dinghy -d raspi test pass [...] # this one shall not pass % cargo dinghy -d raspi test fail [...] ``` That's it! Enjoy! ================================================ FILE: docs/vars.md ================================================ ### Environment variables Dinghy allows defining environment variables per-platform. These variables will be set during the whole build process (including build.rs scripts targeting either the host or target platforms). ```toml [platforms.my-platform] env={ MY_ENV="my-value" } ``` Here is an example environment variable that helps running openssl build script for the host platform (when another dependency build script uses openssl to download some stuff) during an android cross compilation build: ```toml [platforms.android-arm64] env={ X86_64_UNKNOWN_LINUX_GNU_OPENSSL_DIR = "/usr" } ``` It's possible to setup environment variables for a (non cross-compilation) build running for the host platform too: ```toml [platforms.host] env={ MY_ENV="my-value" } ``` ================================================ FILE: legacy/Cargo.toml ================================================ [package] name = "dinghy" version = "0.3.0" edition = "2021" authors = ["Mathieu Poumeyrol ", "Thibaut Lorrain "] license = "MIT/Apache-2.0" description = "[Deprecated] use cargo-dinghy instead" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] [workspace] ================================================ FILE: legacy/README.md ================================================ # Deprecated Dinghy used to be deployed on the `dinghy` name on crates.io, we've since moved to `cargo-dinghy` to have the same name as the binary. The use of the `dinghy` crate is deprecated, use `cargo-dinghy` instead. Install it with the following command: ``` $ cargo install cargo-dinghy ``` This crate is just a placeholder to be deployed on crates.io with the `dinghy` name, it fails on compilation to prompt users to install the correct crate. ================================================ FILE: legacy/build.rs ================================================ fn main() { panic!(" The use of the dinghy crate is deprecated, use cargo-dinghy instead. Install it with the following command: \t$ cargo install cargo-dinghy "); } ================================================ FILE: legacy/src/main.rs ================================================ fn main() {} ================================================ FILE: musl_build.sh ================================================ #!/bin/sh set -ex RUST_TRIPLE=x86_64-unknown-linux-musl rustup target add $RUST_TRIPLE cargo build --target $RUST_TRIPLE --release -p cargo-dinghy mv target/${RUST_TRIPLE}/release/cargo-dinghy target/cargo-dinghy ================================================ FILE: post-release.sh ================================================ #!/bin/sh VERSION=$1 CRATES="dinghy-build dinghy-test dinghy-lib cargo-dinghy" if [ -z "$VERSION" ] then echo "Usage: $0 " exit 1 fi # set_version cargo-dinghy/Cargo.toml 0.3.0 set_version() { FILE=$1 VERSION=$2 sed -i.back "s/^version *= *\".*\"/version = \"$2\"/" $FILE sed -i.back "s/^\(dinghy-[^ =]*\).*/\\1 = { path = \"..\/\\1\" }/" $FILE } set -ex for c in $CRATES do set_version $c/Cargo.toml $VERSION done (cd cargo-dinghy ; cargo update) (cd test-ws ; cargo update) git commit . -m "post-release $VERSION" git push ================================================ FILE: pre-release.sh ================================================ #!/usr/bin/env bash set -e NEW_VERSION=$1 if [[ -z "${NEW_VERSION}" ]]; then echo "usage: $0 " exit 1 fi if [[ -z "${CHANGELOG_GITHUB_TOKEN}" ]]; then echo "You need to set a CHANGELOG_GITHUB_TOKEN environment variable with a Github token" echo "Click this link to generate the token, it doesn't need any additional scopes" echo " https://github.com/settings/tokens/new?description=Dinghy%20Changelog%20Generator" exit 1 fi if ! gem list -i github_changelog_generator > /dev/null; then echo "Please install the ruby gem github_changelog_generator using the following command:" echo " gem install github_changelog_generator" exit 1 fi if ! git diff-index --quiet HEAD --; then echo "Git workspace is not clean, clean it before proceeding" exit 1 fi echo "Generating changelog" PATH="$(gem environment user_gemhome)/bin:$(gem environment gemhome)/bin:$PATH" github_changelog_generator --no-verbose -u sonos -p dinghy --include-tags-regex "^0\..+\..+" --future-release "${NEW_VERSION}" printf "Here are the updates to the changelog \n\n\n" git --no-pager diff printf "\n\n\nDoes this seem correct (Y/n)? " read -r ANSWER if [[ -n "${ANSWER}" ]] && ! [[ "${ANSWER}" == [Yy]* ]]; then echo "Aborting, go fix things" exit 1 fi git commit -am "changelog for $NEW_VERSION" ================================================ FILE: release.sh ================================================ #!/usr/bin/env bash CRATE=$1 VERSION=$2 CRATES="dinghy-build dinghy-test dinghy-lib cargo-dinghy" if [ -z "$VERSION" ] then echo "Usage: $0 " echo crates order is: $CRATES exit 1 fi set -ex if ! [[ $(git --no-pager log HEAD^..HEAD --pretty=format:"%s") == "changelog for $VERSION" ]] && [[ -z "${SKIP_CHANGELOG_CHECK}" ]] then echo "Please generate the changelog first using ./pre-release $VERSION" exit 1 fi if [ "$CRATE" = "all" ] then for c in $CRATES do SKIP_CHANGELOG_CHECK=1 $0 $c $VERSION done exit 0 fi # set_version cargo-dinghy/Cargo.toml 0.3.0 set_version() { FILE=$1 VERSION=$2 sed -i.back "s/^version *= *\".*\"/version = \"$2\"/" $FILE sed -i.back "s/^\(dinghy-[^ =]*\).*/\\1 = \"$2\"/" $FILE } set_version $CRATE/Cargo.toml $VERSION git commit . -m "release $CRATE/$VERSION" (cd $CRATE ; cargo publish) if [ "$CRATE" = "cargo-dinghy" ] then git tag "cargo-dinghy/$VERSION" git push --tags fi sleep 15 ================================================ FILE: rust-toolchain ================================================ 1.85.0 ================================================ FILE: test-ws/.dinghy.toml ================================================ [test_data] dinghy_license = { source = "../LICENSE", copy_git_ignored = true } ================================================ FILE: test-ws/Cargo.toml ================================================ [workspace] members = ["test-app", "test-proc-macro", "test-bin"] resolver = "2" ================================================ FILE: test-ws/rust-toolchain ================================================ 1.89.0 ================================================ FILE: test-ws/test-app/.dinghy.toml ================================================ [test_data] dinghy_source = "../.." ================================================ FILE: test-ws/test-app/Cargo.toml ================================================ [package] name = "test-app" version = "0.1.0" authors = ["Mathieu Poumeyrol "] edition = "2021" workspace = "../" [features] my-feature = [] [dev-dependencies] dinghy-test = { path = "../../dinghy-test" } ================================================ FILE: test-ws/test-app/src/lib.rs ================================================ #[cfg(test)] extern crate dinghy_test; /// this is an example doctest with lots of code blocks that will each generate a separate test. /// /// ``` /// assert_eq!(2 + 2, 4); /// ``` /// /// ``` /// assert_eq!(3 + 3, 6); /// ``` /// /// ``` /// assert_eq!(4 + 4, 8); /// ``` /// /// ``` /// assert_eq!(5 + 5, 10); /// ``` /// /// ``` /// assert_eq!(6 + 6, 12); /// ``` /// /// ``` /// assert_eq!(7 + 7, 14); /// ``` pub fn example_function() -> i32 { 2 + 2 } #[cfg(test)] mod tests { mod pass { use dinghy_test::test_file_path; use dinghy_test::test_project_path; use dinghy_test::try_test_file_path; use std::path; #[test] fn it_finds_source_files() { println!("pwd: {:?}", ::std::env::current_dir()); println!("test_project_path: {:?}", test_project_path()); assert!(test_project_path().join("src/lib.rs").exists()); } #[test] fn it_finds_test_data_files() { println!("pwd: {:?}", ::std::env::current_dir()); let license = if let Err(_) = ::std::env::var("NOT_BUILT_WITH_DINGHY") { test_file_path("dinghy_source").join("LICENSE") } else { try_test_file_path("dinghy_source") .unwrap_or(path::PathBuf::from("../..")) .join("LICENSE") }; println!("Found path: {:?}", license); assert!( license.exists(), "File from dinghy_source not found: {:?}", license ); let license = if let Err(_) = ::std::env::var("NOT_BUILT_WITH_DINGHY") { test_file_path("dinghy_license") } else { try_test_file_path("dinghy_license").unwrap_or(path::PathBuf::from("../../LICENSE")) }; println!("Found path: {:?}", license); assert!( license.exists(), "File dinghy_license not found: {:?}", license ); } #[test] fn it_works() {} } mod fails { #[test] fn it_fails() { panic!("Failing as expected"); } } mod feature { #[test] fn has_feature() { assert!(cfg!(feature = "my-feature")) } } } ================================================ FILE: test-ws/test-bin/Cargo.toml ================================================ [package] name = "test-bin" version = "0.1.0" edition = "2021" workspace = "../" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] ================================================ FILE: test-ws/test-bin/src/main.rs ================================================ fn main() { for (name, value) in std::env::vars() { println!("env {} -> {}", name, value); } println!("Hello, world!"); } ================================================ FILE: test-ws/test-proc-macro/Cargo.toml ================================================ [package] name = "test-proc-macro" version = "0.1.0" edition = "2021" workspace = "../" [lib] proc-macro = true ================================================ FILE: test-ws/test-proc-macro/src/lib.rs ================================================