├── .github └── workflows │ ├── bump.yml │ ├── check.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── plugin ├── build.gradle └── src │ ├── main │ ├── groovy │ │ └── com │ │ │ └── nishtahir │ │ │ └── Versions.groovy │ ├── kotlin │ │ └── com │ │ │ └── nishtahir │ │ │ ├── CargoBuildTask.kt │ │ │ ├── CargoExtension.kt │ │ │ ├── Extensions.kt │ │ │ ├── GenerateLinkerWrapperTask.kt │ │ │ ├── GenerateToolchainsTask.kt │ │ │ └── RustAndroidPlugin.kt │ └── resources │ │ └── com │ │ └── nishtahir │ │ ├── linker-wrapper.bat │ │ ├── linker-wrapper.py │ │ └── linker-wrapper.sh │ └── test │ ├── groovy │ └── com │ │ └── nishtahir │ │ ├── AbstractTest.groovy │ │ ├── CargoBuildTest.groovy │ │ ├── CargoTargetTest.groovy │ │ ├── MultiVersionTest.groovy │ │ ├── NdkVersionTest.groovy │ │ ├── SimpleAndroidApp.groovy │ │ ├── SimpleCargoProject.groovy │ │ └── TestVersions.groovy │ └── resources │ ├── SpockConfig.groovy │ └── rust │ ├── .cargo │ └── config │ ├── Cargo.toml │ └── src │ └── lib.rs ├── samples ├── app │ ├── build.gradle │ ├── settings.gradle │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── nishtahir │ │ │ └── androidrust │ │ │ └── ExampleInstrumentedTest.java │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── nishtahir │ │ │ └── androidrust │ │ │ ├── JNICallback.java │ │ │ └── MainActivity.java │ │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml ├── library │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ ├── settings.gradle │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── nishtahir │ │ │ └── library │ │ │ └── ExampleInstrumentedTest.java │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── res │ │ └── values │ │ └── strings.xml ├── rust │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── unittest │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nishtahir │ │ └── androidrust │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nishtahir │ │ │ └── androidrust │ │ │ ├── JNACallback.java │ │ │ ├── JNICallback.java │ │ │ └── MainActivity.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── nishtahir │ └── androidrust │ └── ExampleUnitTest.java ├── settings.gradle └── version.properties /.github/workflows/bump.yml: -------------------------------------------------------------------------------- 1 | name: Bump version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | new_version: 7 | type: string 8 | description: New version number, like x.y.z 9 | required: true 10 | changelog: 11 | type: string 12 | description: Single line of details to prepend to `CHANGELOG`. 13 | required: true 14 | 15 | jobs: 16 | bump-version: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | # Everything here was cribbed from or inspired by 23 | # https://github.com/oflynned/android-version-bump/blob/b9f6de7f8bdf25de3f695843265debf7c3919272. 24 | - name: Bump version 25 | id: bump_version 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: | 29 | old_version=$(sed -e 's/.*=//' version.properties) 30 | echo "Bumping old version: $old_version to new version ${{ github.event.inputs.new_version }}." 31 | echo "Changelog:" 32 | echo "- ${{ github.event.inputs.changelog }}" 33 | 34 | echo "# ${{ github.event.inputs.new_version }}" >> new_CHANGELOG.md 35 | echo >> new_CHANGELOG.md 36 | echo "- ${{ github.event.inputs.changelog }}" >> new_CHANGELOG.md 37 | echo >> new_CHANGELOG.md 38 | cat CHANGELOG.md >> new_CHANGELOG.md 39 | mv new_CHANGELOG.md CHANGELOG.md 40 | 41 | sed -i -e "s/$old_version/${{ github.event.inputs.new_version }}/" version.properties README.md 42 | 43 | echo "version=${{ github.event.inputs.new_version }}" > version.properties 44 | 45 | git diff 46 | 47 | git config --global user.name "${GITHUB_USER:-Automated Version Bump}" 48 | git config --global user.email "${GITHUB_EMAIL:-rust-android-gradle@users.noreply.github.com}" 49 | 50 | git add --all 51 | git commit -m "Prep for releasing version ${{ github.event.inputs.new_version }}." 52 | 53 | git log -n 1 54 | 55 | git tag "v${{ github.event.inputs.new_version }}" 56 | remote="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 57 | 58 | git push "${remote}" --follow-tags 59 | git push "${remote}" --tags 60 | 61 | echo "::set-output name=new_tag::v${{ github.event.inputs.new_version }}" 62 | 63 | - name: Create release 64 | id: create_release 65 | uses: actions/create-release@v1 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | with: 69 | tag_name: ${{ steps.bump_version.outputs.new_tag }} 70 | release_name: ${{ steps.bump_version.outputs.new_tag }} 71 | body: "- ${{ github.event.inputs.changelog }}" 72 | prerelease: false 73 | draft: true 74 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull 4 | # request events, but only for the `master` branch (generally) or the `citest` 5 | # branch (for testing). 6 | on: 7 | push: 8 | branches: [master, citest] 9 | pull_request: 10 | branches: [master] 11 | schedule: 12 | # N.b. * is a special character in YAML so you have to quote this 13 | # string. 10:20 UTC (2:20 Pacific), once a week on Tuesday, to 14 | # verify this stays healthy over time. 15 | - cron: '20 10 * * 3' 16 | 17 | jobs: 18 | generate_versions: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE for the job. 23 | - uses: actions/checkout@v2 24 | 25 | - name: Gradle test 26 | run: | 27 | ./gradlew -p plugin generateTestTasksJson 28 | 29 | - id: setup-matrix 30 | run: echo "::set-output name=matrix::$(cat plugin/build/build-resources/androidTestTasks.json)" 31 | 32 | - name: debug 33 | run: echo ${{ steps.setup-matrix.outputs.matrix }} 34 | 35 | outputs: 36 | matrix: ${{ steps.setup-matrix.outputs.matrix }} 37 | 38 | samples: 39 | # The type of runner that the job will run on 40 | runs-on: ${{ matrix.os }} 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | os: 46 | - ubuntu-latest 47 | - macos-latest 48 | - windows-latest 49 | 50 | steps: 51 | # Checks-out your repository under $GITHUB_WORKSPACE for the job. 52 | - uses: actions/checkout@v2 53 | 54 | - name: Setup Rust 55 | run: | 56 | rustup toolchain install stable 57 | rustup target add x86_64-linux-android 58 | rustup target add x86_64-unknown-linux-gnu 59 | rustup target add aarch64-linux-android 60 | 61 | - name: Setup Java 11 62 | uses: actions/setup-java@v2 63 | with: 64 | distribution: 'temurin' 65 | java-version: 11 66 | cache: 'gradle' 67 | 68 | - name: Assemble samples/app 69 | run: | 70 | ./gradlew -p samples/app :assembleDebug --info --warning-mode all 71 | 72 | - name: Assemble samples/library 73 | run: | 74 | ./gradlew -p samples/library :assembleDebug --info --warning-mode all 75 | 76 | # Work around https://github.com/actions/cache/issues/454. 77 | - name: Gradle stop 78 | run: | 79 | ./gradlew --stop 80 | 81 | android_unversioned_tests: 82 | # The type of runner that the job will run on 83 | runs-on: ${{ matrix.os }} 84 | 85 | strategy: 86 | fail-fast: false 87 | matrix: 88 | os: 89 | - ubuntu-latest 90 | - macos-latest 91 | - windows-latest 92 | 93 | steps: 94 | # Checks-out your repository under $GITHUB_WORKSPACE for the job. 95 | - uses: actions/checkout@v2 96 | 97 | - name: Setup Rust 98 | run: | 99 | rustup toolchain install stable 100 | rustup target add x86_64-linux-android 101 | rustup target add x86_64-unknown-linux-gnu 102 | rustup target add aarch64-linux-android 103 | 104 | # Use Java 8 105 | - name: Setup Java 8 106 | uses: actions/setup-java@v2 107 | with: 108 | distribution: 'temurin' 109 | java-version: 8 110 | cache: 'gradle' 111 | 112 | - name: Gradle setup 113 | run: | 114 | ./gradlew -p plugin tasks --warning-mode all 115 | 116 | - name: Gradle test 117 | run: | 118 | ./gradlew -p plugin test --info --warning-mode all 119 | 120 | # Work around https://github.com/actions/cache/issues/454. 121 | - name: Gradle stop 122 | run: | 123 | ./gradlew --stop 124 | 125 | android_version_tests: 126 | needs: [generate_versions] # , sanity_check] 127 | 128 | # The type of runner that the job will run on 129 | runs-on: ${{ matrix.os }} 130 | 131 | strategy: 132 | fail-fast: false 133 | matrix: 134 | os: 135 | - ubuntu-latest 136 | - macos-latest 137 | - windows-latest 138 | androidTestTask: ${{ fromJson(needs.generate_versions.outputs.matrix) }} 139 | 140 | # Steps represent a sequence of tasks that will be executed as part of the job 141 | steps: 142 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 143 | - uses: actions/checkout@v2 144 | 145 | - name: Setup Rust 146 | run: | 147 | rustup toolchain install stable 148 | rustup target add x86_64-linux-android 149 | rustup target add x86_64-unknown-linux-gnu 150 | rustup target add aarch64-linux-android 151 | 152 | # Use Java 8 153 | - name: Setup Java 8 154 | uses: actions/setup-java@v2 155 | with: 156 | distribution: 'temurin' 157 | java-version: 8 158 | cache: 'gradle' 159 | 160 | - name: Gradle setup 161 | run: | 162 | ./gradlew -p plugin tasks --warning-mode all 163 | 164 | - name: Gradle test 165 | run: | 166 | ./gradlew -p plugin ${{ matrix.androidTestTask }} --info --warning-mode all 167 | 168 | # Work around https://github.com/actions/cache/issues/454. 169 | - name: Gradle stop 170 | run: | 171 | ./gradlew --stop 172 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish plugin 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Set up JDK 8 15 | uses: actions/setup-java@v2 16 | with: 17 | java-version: '8' 18 | distribution: 'temurin' 19 | 20 | - name: Write gradle.properties 21 | env: 22 | GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} 23 | GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} 24 | run: | 25 | mkdir -p $HOME/.gradle/ 26 | echo "gradle.publish.key=${GRADLE_PUBLISH_KEY}" >> ~/.gradle/gradle.properties 27 | echo "gradle.publish.secret=${GRADLE_PUBLISH_SECRET}" >> ~/.gradle/gradle.properties 28 | 29 | - name: Publish plugin 30 | run: | 31 | ./gradlew publishPlugins 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.apk 2 | *.ap_ 3 | *.dex 4 | *.class 5 | bin/ 6 | gen/ 7 | out/ 8 | .gradle/ 9 | build/ 10 | local.properties 11 | proguard/ 12 | *.log 13 | .navigation/ 14 | captures/ 15 | *.iml 16 | .idea/ 17 | .externalNativeBuild 18 | cmake-build-debug/ 19 | *.iws 20 | /out/ 21 | .idea_modules/ 22 | atlassian-ide-plugin.xml 23 | .idea/replstate.xml 24 | com_crashlytics_export_strings.xml 25 | crashlytics.properties 26 | crashlytics-build.properties 27 | fabric.properties 28 | modules.xml 29 | .idea/misc.xml 30 | *.ipr 31 | /target/ 32 | /samples/rust/target/ 33 | Cargo.lock 34 | **/*.rs.bk 35 | .gradle 36 | **/build/ 37 | gradle-app.setting 38 | !gradle-wrapper.jar 39 | .gradletasknamecache 40 | 41 | .cargo 42 | **/maven-repo/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.9.6 2 | 3 | - Added option to add a build-id during linking 4 | 5 | # 0.9.5 6 | 7 | - Do not use deprecated module for Python 3.13 compatibility 8 | 9 | # 0.9.4 10 | 11 | - Always use the NDK version from . 12 | 13 | # 0.9.3 14 | 15 | - Fix issues with Android NDK 23+: use `llvm-ar`, support `-lgcc` rewriting on Windows. 16 | 17 | # 0.9.2 18 | 19 | - Support Android NDK 23. 20 | 21 | # 0.9.1 22 | 23 | - Add Desktop targets `darwin-x86-64`, which will supersede `darwin`, and `darwin-aarch64`. 24 | 25 | # 0.9.0 26 | 27 | - Support multiple Android Gradle Plugin versions. Everything here is based on https://github.com/gradle/android-cache-fix-gradle-plugin; many thanks to that project for paving the way here. 28 | - Allow `module` and `targetDirectory` to be absolute paths. This is used in the test harness at the moment. 29 | 30 | # 0.8.7 31 | 32 | - Use per-platform API level for selecting toolchain. 33 | 34 | # 0.8.6 35 | 36 | - Revert a change that prevented publishing. 37 | 38 | # 0.8.5 39 | 40 | - Allow to use `+nightly`, etc, with `cargo { rustupChannel = "..." }`. Fixes #24. 41 | - Allow to set `cargo { (cargo|python|rustc)Command = "..." }`. Fixes #48. 42 | 43 | # 0.8.4 44 | 45 | - The plugin tries to interoperate with Rust bindgen out of the box by setting `CLANG_PATH`. 46 | - We no longer invoke `cargo` for the Gradle `clean` target. 47 | 48 | # 0.8.3 49 | 50 | - Plugin now supports using prebuilt NDK toolchains. 51 | 52 | # 0.8.2 53 | 54 | - Avoid passing `--target` to cargo for the default target. 55 | - The `exec` callback is now invoked as late as possible. 56 | - The `CARGO_TARGET_DIR` environment variable should now be respected, if it is set. 57 | - Various parts of the plugin's documentation have been improved. 58 | 59 | # 0.8.1 60 | 61 | - Added `extraCargoBuildArguments`. 62 | 63 | # 0.8.0 64 | 65 | - **breaking** Further split "win32-x86-64" into "win32-x86-64-{gnu,msvc}". 66 | - Fixed bug with DLL libraries in with JNA: expect "foo.dll" instead 67 | of "libfoo.dll". 68 | 69 | # 0.7.0 70 | 71 | - Added per-target pass-through variables. 72 | - Separated "default" target into multiple Desktop targets: 73 | "linux-x86-64, "darwin", "win32-x86-64". 74 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Nish Tahir 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Android Gradle Plugin 2 | 3 | Cross compile Rust Cargo projects for Android targets. 4 | 5 | 6 |

7 | 8 | 9 |

10 | 11 | # Usage 12 | 13 | Add the plugin to your root `build.gradle`, like: 14 | 15 | ```groovy 16 | buildscript { 17 | repositories { 18 | maven { 19 | url "https://plugins.gradle.org/m2/" 20 | } 21 | } 22 | dependencies { 23 | classpath 'org.mozilla.rust-android-gradle:plugin:0.9.6' 24 | } 25 | } 26 | ``` 27 | 28 | or 29 | 30 | ```groovy 31 | buildscript { 32 | //... 33 | } 34 | 35 | plugins { 36 | id "org.mozilla.rust-android-gradle.rust-android" version "0.9.6" 37 | } 38 | ``` 39 | 40 | In your *project's* build.gradle, `apply plugin` and add the `cargo` configuration: 41 | 42 | ```groovy 43 | android { ... } 44 | 45 | apply plugin: 'org.mozilla.rust-android-gradle.rust-android' 46 | 47 | cargo { 48 | module = "../rust" // Or whatever directory contains your Cargo.toml 49 | libname = "rust" // Or whatever matches Cargo.toml's [package] name. 50 | targets = ["arm", "x86"] // See bellow for a longer list of options 51 | } 52 | ``` 53 | 54 | Install the rust toolchains for your target platforms: 55 | 56 | ```sh 57 | rustup target add armv7-linux-androideabi # for arm 58 | rustup target add i686-linux-android # for x86 59 | rustup target add aarch64-linux-android # for arm64 60 | rustup target add x86_64-linux-android # for x86_64 61 | rustup target add x86_64-unknown-linux-gnu # for linux-x86-64 62 | rustup target add x86_64-apple-darwin # for darwin x86_64 (if you have an Intel MacOS) 63 | rustup target add aarch64-apple-darwin # for darwin arm64 (if you have a M1 MacOS) 64 | rustup target add x86_64-pc-windows-gnu # for win32-x86-64-gnu 65 | rustup target add x86_64-pc-windows-msvc # for win32-x86-64-msvc 66 | ... 67 | ``` 68 | 69 | Finally, run the `cargoBuild` task to cross compile: 70 | ```sh 71 | ./gradlew cargoBuild 72 | ``` 73 | Or add it as a dependency to one of your other build tasks, to build your rust code when you normally build your project: 74 | ```gradle 75 | tasks.whenTaskAdded { task -> 76 | if ((task.name == 'javaPreCompileDebug' || task.name == 'javaPreCompileRelease')) { 77 | task.dependsOn 'cargoBuild' 78 | } 79 | } 80 | ``` 81 | 82 | ## Configuration 83 | 84 | The `cargo` Gradle configuration accepts many options. 85 | 86 | ### Linking Java code to native libraries 87 | 88 | Generated libraries will be added to the Android `jniLibs` source-sets, when correctly referenced in 89 | the `cargo` configuration through the `libname` and/or `targetIncludes` options. The latter 90 | defaults to `["lib${libname}.so", "lib${libname}.dylib", "{$libname}.dll"]`, so the following configuration will 91 | include all `libbackend` libraries generated in the Rust project in `../rust`: 92 | 93 | ``` 94 | cargo { 95 | module = "../rust" 96 | libname = "backend" 97 | } 98 | ``` 99 | 100 | Now, Java code can reference the native library using, e.g., 101 | 102 | ```java 103 | static { 104 | System.loadLibrary("backend"); 105 | } 106 | ``` 107 | 108 | ### Native `apiLevel` 109 | 110 | The [Android NDK](https://developer.android.com/ndk/guides/stable_apis) also fixes an API level, 111 | which can be specified using the `apiLevel` option. This option defaults to the minimum SDK API 112 | level. As of API level 21, 64-bit builds are possible; and conversely, the `arm64` and `x86_64` 113 | targets require `apiLevel >= 21`. 114 | 115 | ### Cargo release profile 116 | 117 | The `profile` option selects between the `--debug` and `--release` profiles in `cargo`. *Defaults 118 | to `debug`!* 119 | 120 | ### Extension reference 121 | 122 | ### module 123 | 124 | The path to the Rust library to build with Cargo; required. `module` can be absolute; if it is not, 125 | it is interpreted as a path relative to the Gradle `projectDir`. 126 | 127 | ```groovy 128 | cargo { 129 | // Note: path is either absolute, or relative to the gradle project's `projectDir`. 130 | module = '../rust' 131 | } 132 | ``` 133 | 134 | ### libname 135 | 136 | The library name produced by Cargo; required. 137 | 138 | `libname` is used to determine which native libraries to include in the produced AARs and/or APKs. 139 | See also [`targetIncludes`](#targetincludes). 140 | 141 | `libname` is also used to determine the ELF SONAME to declare in the Android libraries produced by 142 | Cargo. Different versions of the Android system linker 143 | [depend on the ELF SONAME](https://android-developers.googleblog.com/2016/06/android-changes-for-ndk-developers.html). 144 | 145 | In `Cargo.toml`: 146 | 147 | ```toml 148 | [lib] 149 | name = "test" 150 | ``` 151 | 152 | In `build.gradle`: 153 | 154 | ```groovy 155 | cargo { 156 | libname = 'test' 157 | } 158 | ``` 159 | 160 | ### targets 161 | 162 | A list of Android targets to build with Cargo; required. 163 | 164 | Valid targets for **Android** are: 165 | 166 | ``` 167 | 'arm', 168 | 'arm64', 169 | 'x86', 170 | 'x86_64' 171 | ``` 172 | Valid targets for **Desktop** are: 173 | ``` 174 | 'linux-x86-64', 175 | 'darwin-x86-64', 176 | 'darwin-aarch64', 177 | 'win32-x86-64-gnu', 178 | 'win32-x86-64-msvc' 179 | ``` 180 | 181 | The desktop targets are useful for testing native code in Android unit tests that run on the host, 182 | not on the target device. Better support for this feature is 183 | [planned](https://github.com/ncalexan/rust-android-gradle/issues/13). 184 | 185 | ```groovy 186 | cargo { 187 | targets = ['arm', 'x86', 'linux-x86-64'] 188 | } 189 | ``` 190 | 191 | ### prebuiltToolchains 192 | 193 | When set to `true` (which requires NDK version 19+), use the prebuilt toolchains bundled with the 194 | NDK. When set to `false`, generate per-target architecture standalone NDK toolchains using 195 | `make_standalone_toolchain.py`. When unset, use the prebuilt toolchains if the NDK version is 19+, 196 | and fall back to generated toolchains for older NDK versions. 197 | 198 | Defaults to `null`. 199 | 200 | ```groovy 201 | cargo { 202 | prebuiltToolchains = true 203 | } 204 | ``` 205 | 206 | ### verbose 207 | 208 | When set, execute `cargo build` with or without the `--verbose` flag. When unset, respect the 209 | Gradle log level: execute `cargo build` with or without the `--verbose` flag according to whether 210 | the log level is at least `INFO`. In practice, this makes `./gradlew ... --info` (and `./gradlew 211 | ... --debug`) execute `cargo build --verbose ...`. 212 | 213 | Defaults to `null`. 214 | 215 | ```groovy 216 | cargo { 217 | verbose = true 218 | } 219 | ``` 220 | 221 | ### profile 222 | 223 | The Cargo [release profile](https://doc.rust-lang.org/book/second-edition/ch14-01-release-profiles.html#customizing-builds-with-release-profiles) to build. 224 | 225 | Defaults to `"debug"`. 226 | 227 | ```groovy 228 | cargo { 229 | profile = 'release' 230 | } 231 | ``` 232 | 233 | ### features 234 | 235 | Set the Cargo [features](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section). 236 | 237 | Defaults to passing no flags to `cargo`. 238 | 239 | To pass `--all-features`, use 240 | ```groovy 241 | cargo { 242 | features { 243 | all() 244 | } 245 | } 246 | ``` 247 | 248 | To pass an optional list of `--features`, use 249 | ```groovy 250 | cargo { 251 | features { 252 | defaultAnd("x") 253 | defaultAnd("x", "y") 254 | } 255 | } 256 | ``` 257 | 258 | To pass `--no-default-features`, and an optional list of replacement `--features`, use 259 | ```groovy 260 | cargo { 261 | features { 262 | noDefaultBut() 263 | noDefaultBut("x") 264 | noDefaultBut "x", "y" 265 | } 266 | } 267 | ``` 268 | 269 | ### targetDirectory 270 | 271 | The target directory into which Cargo writes built outputs. You will likely need to specify this 272 | if you are using a [cargo virtual workspace](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html), 273 | as our default will likely fail to locate the correct target directory. 274 | 275 | Defaults to `${module}/target`. `targetDirectory` can be absolute; if it is not, it is interpreted 276 | as a path relative to the Gradle `projectDir`. 277 | 278 | Note that if `CARGO_TARGET_DIR` (see https://doc.rust-lang.org/cargo/reference/environment-variables.html) 279 | is specified in the environment, it takes precedence over `targetDirectory`, as cargo will output 280 | all build artifacts to it, regardless of what is being built, or where it was invoked. 281 | 282 | You may also override `CARGO_TARGET_DIR` variable by setting `rust.cargoTargetDir` in 283 | `local.properties`, however it seems very unlikely that this will be useful, as we don't pass this 284 | information to cargo itself. That said, it can be used to control where we search for the built 285 | library on a per-machine basis. 286 | 287 | ```groovy 288 | cargo { 289 | // Note: path is either absolute, or relative to the gradle project's `projectDir`. 290 | targetDirectory = 'path/to/workspace/root/target' 291 | } 292 | ``` 293 | 294 | ### targetIncludes 295 | 296 | Which Cargo outputs to consider JNI libraries. 297 | 298 | Defaults to `["lib${libname}.so", "lib${libname}.dylib", "{$libname}.dll"]`. 299 | 300 | ```groovy 301 | cargo { 302 | targetIncludes = ['libnotlibname.so'] 303 | } 304 | ``` 305 | 306 | ### apiLevel 307 | 308 | The Android NDK API level to target. NDK API levels are not the same as SDK API versions; they are 309 | updated less frequently. For example, SDK API versions 18, 19, and 20 all target NDK API level 18. 310 | 311 | Defaults to the minimum SDK version of the Android project's default configuration. 312 | 313 | ```groovy 314 | cargo { 315 | apiLevel = 21 316 | } 317 | ``` 318 | 319 | You may specify the API level per target in `targets` using the `apiLevels` option. At most one of 320 | `apiLevel` and `apiLevels` may be specified. `apiLevels` must have an entry for each target in 321 | `targets`. 322 | 323 | ```groovy 324 | cargo { 325 | targets = ["arm", "x86_64"] 326 | apiLevels = [ 327 | "arm": 16, 328 | "x86_64": 21, 329 | ] 330 | } 331 | ``` 332 | 333 | ### extraCargoBuildArguments 334 | 335 | Sometimes, you need to do things that the plugin doesn't anticipate. Use `extraCargoBuildArguments` 336 | to append a list of additional arguments to each `cargo build` invocation. 337 | 338 | ```groovy 339 | cargo { 340 | extraCargoBuildArguments = ['a', 'list', 'of', 'strings'] 341 | } 342 | ``` 343 | 344 | ### generateBuildId 345 | 346 | Generate a build-id for the shared library during the link phase. 347 | 348 | ### exec 349 | 350 | This is a callback taking the `ExecSpec` we're going to use to invoke `cargo build`, and 351 | the relevant toolchain. It's called for each invocation of `cargo build`. This generally 352 | is useful for the following scenarios: 353 | 354 | 1. Specifying target-specific environment variables. 355 | 1. Adding target-specific flags to the command line. 356 | 1. Removing/modifying environment variables or command line options the rust-android-gradle plugin would 357 | provide by default. 358 | 359 | ```groovy 360 | cargo { 361 | exec { spec, toolchain -> 362 | if (toolchain.target != "x86_64-apple-darwin") { 363 | // Don't statically link on macOS desktop builds, for some 364 | // entirely hypothetical reason. 365 | spec.environment("EXAMPLELIB_STATIC", "1") 366 | } 367 | } 368 | } 369 | ``` 370 | 371 | ## Specifying NDK toolchains 372 | 373 | The plugin can either use prebuilt NDK toolchain binaries, or search for (and if missing, build) 374 | NDK toolchains as generated by `make_standalone_toolchain.py`. 375 | 376 | A prebuilt NDK toolchain will be used if: 377 | 1. `rust.prebuiltToolchain=true` in the per-(multi-)project `${rootDir}/local.properties` 378 | 1. `prebuiltToolchain=true` in the `cargo { ... }` block (if not overridden by `local.properties`) 379 | 1. The discovered NDK is version 19 or higher (if not overridden per above) 380 | 381 | The toolchains are rooted in a single Android NDK toolchain directory. In order of preference, the 382 | toolchain root directory is determined by: 383 | 384 | 1. `rust.androidNdkToolchainDir` in the per-(multi-)project `${rootDir}/local.properties` 385 | 1. the environment variable `ANDROID_NDK_TOOLCHAIN_DIR` 386 | 1. `${System.getProperty(java.io.tmpdir)}/rust-android-ndk-toolchains` 387 | 388 | Note that the Java system property `java.io.tmpdir` is not necessarily `/tmp`, including on macOS hosts. 389 | 390 | Each target architecture toolchain is named like `$arch-$apiLevel`: for example, `arm-16` or `arm64-21`. 391 | 392 | ## Specifying local targets 393 | 394 | When developing a project that consumes `rust-android-gradle` locally, it's often convenient to 395 | temporarily change the set of Rust target architectures. In order of preference, the plugin 396 | determines the per-project targets by: 397 | 398 | 1. `rust.targets.${project.Name}` for each project in `${rootDir}/local.properties` 399 | 1. `rust.targets` in `${rootDir}/local.properties` 400 | 1. the `cargo { targets ... }` block in the per-project `build.gradle` 401 | 402 | The targets are split on `','`. For example: 403 | 404 | ``` 405 | rust.targets.library=linux-x86-64 406 | rust.targets=arm,linux-x86-64,darwin 407 | ``` 408 | 409 | ## Specifying paths to sub-commands (Python, Cargo, and Rustc) 410 | 411 | The plugin invokes Python, Cargo and Rustc. In order of preference, the plugin determines what command to invoke for Python by: 412 | 413 | 1. the value of `cargo { pythonCommand = "..." }`, if non-empty 414 | 1. `rust.pythonCommand` in `${rootDir}/local.properties` 415 | 1. the environment variable `RUST_ANDROID_GRADLE_PYTHON_COMMAND` 416 | 1. the default, `python` 417 | 418 | In order of preference, the plugin determines what command to invoke for Cargo by: 419 | 420 | 1. the value of `cargo { cargoCommand = "..." }`, if non-empty 421 | 1. `rust.cargoCommand` in `${rootDir}/local.properties` 422 | 1. the environment variable `RUST_ANDROID_GRADLE_CARGO_COMMAND` 423 | 1. the default, `cargo` 424 | 425 | In order of preference, the plugin determines what command to invoke for `rustc` by: 426 | 427 | 1. the value of `cargo { rustcCommand = "..." }`, if non-empty 428 | 1. `rust.rustcCommand` in `${rootDir}/local.properties` 429 | 1. the environment variable `RUST_ANDROID_GRADLE_RUSTC_COMMAND` 430 | 1. the default, `rustc` 431 | 432 | (Note that failure to locate `rustc` is not fatal, however it may result in rebuilding the code more often than is necessary). 433 | 434 | Paths must be host operating system specific. For example, on Windows: 435 | 436 | ```properties 437 | rust.pythonCommand=c:\Python27\bin\python 438 | ``` 439 | 440 | On Linux, 441 | ```shell 442 | env RUST_ANDROID_GRADLE_CARGO_COMMAND=$HOME/.cargo/bin/cargo ./gradlew ... 443 | ``` 444 | 445 | ## Specifying Rust channel 446 | 447 | Rust is released to three different "channels": stable, beta, and nightly (see 448 | https://rust-lang.github.io/rustup/concepts/channels.html). The `rustup` tool, which is how most 449 | people install Rust, allows multiple channels to be installed simultaneously and to specify which 450 | channel to use by invoking `cargo +channel ...`. 451 | 452 | In order of preference, the plugin determines what channel to invoke `cargo` with by: 453 | 454 | 1. the value of `cargo { rustupChannel = "..." }`, if non-empty 455 | 1. `rust.rustupChannel` in `${rootDir}/local.properties` 456 | 1. the environment variable `RUST_ANDROID_GRADLE_RUSTUP_CHANNEL` 457 | 1. the default, no channel specified (which `cargo` installed via `rustup` generally defaults to the 458 | `stable` channel) 459 | 460 | The channel should be recognized by `cargo` installed via `rustup`, i.e.: 461 | - `"stable"` 462 | - `"beta"` 463 | - `"nightly"` 464 | 465 | A single leading `'+'` will be stripped, if present. 466 | 467 | (Note that Cargo installed by a method other than `rustup` will generally not understand `+channel` 468 | and builds will likely fail.) 469 | 470 | ## Passing arguments to cargo 471 | 472 | The plugin passes project properties named like `RUST_ANDROID_GRADLE_target_..._KEY=VALUE` through 473 | to the Cargo invocation for the given Rust `target` as `KEY=VALUE`. Target should be upper-case 474 | with "-" replaced by "_". (See [the links from this Cargo issue](https://github.com/rust-lang/cargo/issues/5690).) So, for example, 475 | 476 | ```groovy 477 | project.RUST_ANDROID_GRADLE_I686_LINUX_ANDROID_FOO=BAR 478 | ``` 479 | and 480 | ```shell 481 | ./gradlew -PRUST_ANDROID_GRADLE_ARMV7_LINUX_ANDROIDEABI_FOO=BAR ... 482 | ``` 483 | and 484 | ``` 485 | env ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_ARMV7_LINUX_ANDROIDEABI_FOO=BAR ./gradlew ... 486 | ``` 487 | all set `FOO=BAR` in the `cargo` execution environment (for the "armv7-linux-androideabi` Rust 488 | target, corresponding to the "x86" target in the plugin). 489 | 490 | # Development 491 | 492 | At top-level, the `publish` Gradle task updates the Maven repository 493 | under `build/local-repo`: 494 | 495 | ``` 496 | $ ./gradlew publish 497 | ... 498 | $ ls -al build/local-repo/org/mozilla/rust-android-gradle/org.mozilla.rust-android-gradle.gradle.plugin/0.4.0/org.mozilla.rust-android-gradle.gradle.plugin-0.4.0.pom 499 | -rw-r--r-- 1 nalexander staff 670 18 Sep 10:09 500 | build/local-repo/org/mozilla/rust-android-gradle/org.mozilla.rust-android-gradle.gradle.plugin/0.4.0/org.mozilla.rust-android-gradle.gradle.plugin-0.4.0.pom 501 | ``` 502 | 503 | ## Sample projects 504 | 505 | The easiest way to get started is to run the sample projects. The sample projects have dependency 506 | substitutions configured so that changes made to `plugin/` are reflected in the sample projects 507 | immediately. 508 | 509 | ``` 510 | $ ./gradlew -p samples/library :assembleDebug 511 | ... 512 | $ file samples/library/build/outputs/aar/library-debug.aar 513 | samples/library/build/outputs/aar/library-debug.aar: Zip archive data, at least v1.0 to extract 514 | ``` 515 | 516 | ``` 517 | $ ./gradlew -p samples/app :assembleDebug 518 | ... 519 | $ file samples/app/build/outputs/apk/debug/app-debug.apk 520 | samples/app/build/outputs/apk/debug/app-debug.apk: Zip archive data, at least v?[0] to extract 521 | ``` 522 | 523 | ## Testing Local changes 524 | 525 | An easy way to locally test changes made in this plugin is to simply add this to your project's `settings.gradle`: 526 | 527 | ```gradle 528 | // Switch this to point to your local plugin dir 529 | includeBuild('../rust-android-gradle') { 530 | dependencySubstitution { 531 | // As required. 532 | substitute module('gradle.plugin.org.mozilla.rust-android-gradle:plugin') with project(':plugin') 533 | } 534 | } 535 | ``` 536 | 537 | # Publishing 538 | 539 | ## Automatically via the Bump version Github Actions workflow 540 | 541 | You will need to be a collaborator. First, manually invoke the [Bump version Github Actions 542 | workflow](https://github.com/mozilla/rust-android-gradle/actions/workflows/bump.yml). Specify a 543 | version (like "x.y.z", without quotes) and a single line changelog entry. (This entry will have a 544 | dash prepended, so that it would look normal in a list. This is working around [the lack of a 545 | multi-line input in Github 546 | Actions](https://github.community/t/multiline-inputs-for-workflow-dispatch/163906).) This will push 547 | a preparatory commit updating version numbers and the changelog like [this 548 | one](https://github.com/mozilla/rust-android-gradle/commit/2a637d1797a5d0b5063b8d2f0a3d4a4938511154), 549 | and make a **draft** Github Release with a name like `vx.y.z`. After verifying that tests pass, 550 | navigate to [the releases panel](https://github.com/mozilla/rust-android-gradle/releases) and edit 551 | the release, finally pressing "Publish release". The release Github workflow will build and publish 552 | the plugin, although it may take some days for it to be reflected on the Gradle plugin portal. 553 | 554 | ## By hand 555 | 556 | You will need credentials to publish to the [Gradle plugin portal](https://plugins.gradle.org/) in 557 | the appropriate place for the [`plugin-publish`](https://plugins.gradle.org/docs/publish-plugin) to 558 | find them. Usually, that's in `~/.gradle/gradle.properties`. 559 | 560 | At top-level, the `publishPlugins` Gradle task publishes the plugin for consumption: 561 | 562 | ``` 563 | $ ./gradlew publishPlugins 564 | ... 565 | Publishing plugin org.mozilla.rust-android-gradle.rust-android version 0.8.1 566 | Publishing artifact build/libs/plugin-0.8.1.jar 567 | Publishing artifact build/libs/plugin-0.8.1-sources.jar 568 | Publishing artifact build/libs/plugin-0.8.1-javadoc.jar 569 | Publishing artifact build/publish-generated-resources/pom.xml 570 | Activating plugin org.mozilla.rust-android-gradle.rust-android version 0.8.1 571 | ``` 572 | 573 | ## Real projects 574 | 575 | To test in a real project, use the local Maven repository in your `build.gradle`, like: 576 | 577 | ```gradle 578 | buildscript { 579 | repositories { 580 | maven { 581 | url "file:///Users/nalexander/Mozilla/rust-android-gradle/build/local-repo" 582 | } 583 | } 584 | 585 | dependencies { 586 | classpath 'org.mozilla.rust-android-gradle:plugin:0.9.0' 587 | } 588 | } 589 | ``` 590 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | Properties versionProperties = new Properties() 3 | versionProperties.load(new FileInputStream("$project.rootDir/version.properties")) 4 | 5 | ext.kotlin_version = '1.3.50' 6 | ext.agp_version = '4.0.1' 7 | ext.plugin_version = versionProperties.getProperty("version") 8 | 9 | repositories { 10 | google() 11 | maven { 12 | url "https://plugins.gradle.org/m2/" 13 | } 14 | } 15 | dependencies { 16 | classpath "com.android.tools.build:gradle:$agp_version" 17 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 18 | } 19 | } 20 | 21 | subprojects { 22 | repositories { 23 | google() 24 | maven { 25 | url "https://plugins.gradle.org/m2/" 26 | } 27 | } 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | 34 | wrapper { 35 | distributionType = Wrapper.DistributionType.ALL 36 | } 37 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | import groovy.json.JsonBuilder 2 | import org.gradle.util.VersionNumber 3 | 4 | plugins { 5 | id 'com.gradle.plugin-publish' version '0.14.0' 6 | id "org.gradle.test-retry" version "1.2.0" 7 | } 8 | 9 | apply plugin: "java-gradle-plugin" 10 | apply plugin: "maven-publish" 11 | apply plugin: "groovy" 12 | apply plugin: "kotlin" 13 | 14 | gradlePlugin { 15 | plugins { 16 | rustAndroidGradlePlugin { 17 | id = 'org.mozilla.rust-android-gradle.rust-android' 18 | implementationClass = 'com.nishtahir.RustAndroidPlugin' 19 | displayName = 'Plugin for building Rust with Cargo in Android projects' 20 | description = 'A plugin that helps build Rust JNI libraries with Cargo for use in Android projects.' 21 | } 22 | } 23 | } 24 | 25 | group 'org.mozilla.rust-android-gradle' 26 | version "$plugin_version" 27 | 28 | def isCI = (System.getenv('CI') ?: 'false').toBoolean() 29 | 30 | // Maps supported Android plugin versions to the versions of Gradle that support it 31 | def supportedVersions = [ 32 | "7.0.0": ["7.1.1"], 33 | "4.2.2": ["6.8.3", "7.1.1"], 34 | "4.1.3": ["6.5.1", "6.8.3"], 35 | "4.0.2": ["6.1.1", "6.8.3"], 36 | "3.6.4": ["5.6.4", "6.8.3"], 37 | "3.5.4": ["5.4.1", "5.6.4", "6.8.3"], 38 | "3.1.2": ["4.10.2"] 39 | ] 40 | 41 | // A local repo we publish our library to for testing in order to workaround limitations 42 | // in the TestKit plugin classpath. 43 | def localRepo = file("$buildDir/local-repo") 44 | publishing { 45 | repositories { 46 | maven { 47 | url = localRepo.toURI() 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | implementation gradleApi() 54 | compileOnly "com.android.tools.build:gradle:${agp_version}" 55 | 56 | testImplementation gradleTestKit() 57 | testImplementation "com.android.tools.build:gradle:${agp_version}" 58 | testImplementation platform("org.spockframework:spock-bom:2.0-M5-groovy-3.0") 59 | testImplementation("org.spockframework:spock-core") { exclude group: 'org.codehaus.groovy' } 60 | testImplementation("org.spockframework:spock-junit4") { exclude group: 'org.codehaus.groovy' } 61 | testImplementation "org.junit.jupiter:junit-jupiter-api" 62 | } 63 | 64 | compileKotlin { 65 | kotlinOptions.jvmTarget = "1.8" 66 | } 67 | compileTestKotlin { 68 | kotlinOptions.jvmTarget = "1.8" 69 | } 70 | 71 | pluginBundle { 72 | website = 'https://github.com/mozilla/rust-android-gradle' 73 | vcsUrl = 'https://github.com/mozilla/rust-android-gradle.git' 74 | tags = ['rust', 'cargo', 'android'] 75 | } 76 | 77 | 78 | // Generate a json file that contains the matrix of Gradle and AGP versions to test against. 79 | def generatedResources = "$buildDir/generated-resources/main" 80 | tasks.register('generateVersions') { 81 | def outputFile = file("$generatedResources/versions.json") 82 | inputs.property "version", version 83 | inputs.property "supportedVersions", supportedVersions 84 | outputs.dir generatedResources 85 | doLast { 86 | outputFile.text = new JsonBuilder([ 87 | version: version, 88 | supportedVersions: supportedVersions 89 | ]).toPrettyString() 90 | } 91 | } 92 | 93 | sourceSets { 94 | main { 95 | output.dir(generatedResources, builtBy: tasks.named('generateVersions')) 96 | } 97 | } 98 | 99 | // This is used by github actions to split out jobs by Android version test task 100 | def generatedBuildResources = "$buildDir/build-resources" 101 | tasks.register('generateTestTasksJson') { 102 | def outputFile = file("${generatedBuildResources}/androidTestTasks.json") 103 | inputs.property "supportedVersions", supportedVersions 104 | outputs.dir generatedBuildResources 105 | doLast { 106 | outputFile.text = new JsonBuilder( 107 | // Fails in CI with issues invoking Java 11. The single test that 108 | // requires Java 11 succeeds. To be investigated in the future. 109 | // ['test'] + 110 | (supportedVersions.keySet().collect {androidVersion -> androidTestTaskName(androidVersion) }) 111 | ).toString() 112 | } 113 | } 114 | 115 | // Configuration common to all test tasks 116 | tasks.withType(Test).configureEach { 117 | dependsOn publish 118 | systemProperty "local.repo", localRepo.toURI() 119 | useJUnitPlatform() 120 | retry { 121 | maxRetries = isCI ? 1 : 0 122 | maxFailures = 20 123 | } 124 | } 125 | 126 | // Generate a test task for each Android version and run the tests annotated with the MultiVersionTest category 127 | supportedVersions.keySet().each { androidVersion -> 128 | def testTaskName = androidTestTaskName(androidVersion) 129 | def jdkVersion = jdkVersionFor(androidVersion) 130 | def versionSpecificTest = tasks.register(testTaskName, Test) { 131 | description = "Runs the multi-version tests for AGP ${androidVersion} (JDK version ${jdkVersion})" 132 | group = "verification" 133 | 134 | javaLauncher = javaToolchains.launcherFor { 135 | languageVersion = jdkVersion 136 | } 137 | 138 | systemProperty 'org.gradle.android.testVersion', androidVersion 139 | } 140 | 141 | tasks.named('check').configure { 142 | dependsOn versionSpecificTest 143 | } 144 | } 145 | 146 | static def androidTestTaskName(String androidVersion) { 147 | return "testAndroid${normalizeVersion(androidVersion)}" 148 | } 149 | 150 | static def normalizeVersion(String version) { 151 | return version.replaceAll('[.\\-]', '_') 152 | } 153 | 154 | static def jdkVersionFor(String version) { 155 | def jdkVersion = VersionNumber.parse(version) > VersionNumber.parse("7.0.0-alpha01") ? 11 : 8 156 | 157 | return JavaLanguageVersion.of(jdkVersion) 158 | } 159 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/com/nishtahir/Versions.groovy: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import com.google.common.collect.ImmutableMultimap 4 | import com.google.common.collect.ImmutableSortedSet 5 | import com.google.common.collect.Multimap 6 | import groovy.json.JsonSlurper 7 | import groovy.transform.CompileStatic 8 | import groovy.transform.TypeCheckingMode 9 | import org.gradle.util.GradleVersion 10 | import org.gradle.util.VersionNumber 11 | 12 | @CompileStatic(TypeCheckingMode.SKIP) 13 | class Versions { 14 | static final VersionNumber PLUGIN_VERSION; 15 | static final Set SUPPORTED_GRADLE_VERSIONS 16 | static final Set SUPPORTED_ANDROID_VERSIONS 17 | static final Multimap SUPPORTED_VERSIONS_MATRIX 18 | 19 | static { 20 | def versions = new JsonSlurper().parse(Versions.classLoader.getResource("versions.json")) 21 | PLUGIN_VERSION = VersionNumber.parse(versions.version) 22 | 23 | def builder = ImmutableMultimap.builder() 24 | versions.supportedVersions.each { String androidVersion, List gradleVersions -> 25 | builder.putAll(android(androidVersion), gradleVersions.collect { gradle(it) }) 26 | } 27 | def matrix = builder.build() 28 | 29 | SUPPORTED_VERSIONS_MATRIX = matrix 30 | SUPPORTED_ANDROID_VERSIONS = ImmutableSortedSet.copyOf(matrix.keySet()) 31 | SUPPORTED_GRADLE_VERSIONS = ImmutableSortedSet.copyOf(matrix.values()) 32 | } 33 | 34 | static VersionNumber android(String version) { 35 | VersionNumber.parse(version) 36 | } 37 | 38 | static GradleVersion gradle(String version) { 39 | GradleVersion.version(version) 40 | } 41 | 42 | static VersionNumber earliestMaybeSupportedAndroidVersion() { 43 | VersionNumber earliestSupported = SUPPORTED_ANDROID_VERSIONS.min() 44 | // "alpha" is lower than null 45 | return new VersionNumber(earliestSupported.major, earliestSupported.minor, 0, "alpha") 46 | } 47 | 48 | static VersionNumber latestAndroidVersion() { 49 | return SUPPORTED_ANDROID_VERSIONS.max() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/nishtahir/CargoBuildTask.kt: -------------------------------------------------------------------------------- 1 | package com.nishtahir; 2 | 3 | import com.android.build.gradle.* 4 | import org.apache.tools.ant.taskdefs.condition.Os 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.GradleException 7 | import org.gradle.api.Project 8 | import org.gradle.api.logging.LogLevel 9 | import org.gradle.api.tasks.Input 10 | import org.gradle.api.tasks.TaskAction 11 | import java.io.ByteArrayOutputStream 12 | import java.io.File 13 | 14 | open class CargoBuildTask : DefaultTask() { 15 | @Input 16 | var toolchain: Toolchain? = null 17 | 18 | @Input 19 | var ndk: Ndk? = null 20 | 21 | @Suppress("unused") 22 | @TaskAction 23 | fun build() = with(project) { 24 | extensions[CargoExtension::class].apply { 25 | // Need to capture the value to dereference smoothly. 26 | val toolchain = toolchain 27 | if (toolchain == null) { 28 | throw GradleException("toolchain cannot be null") 29 | } 30 | 31 | val ndk = ndk ?: throw GradleException("ndk cannot be null") 32 | 33 | project.plugins.all { 34 | when (it) { 35 | is AppPlugin -> buildProjectForTarget(project, toolchain, ndk, this) 36 | is LibraryPlugin -> buildProjectForTarget(project, toolchain, ndk, this) 37 | } 38 | } 39 | // CARGO_TARGET_DIR can be used to force the use of a global, shared target directory 40 | // across all rust projects on a machine. Use it if it's set, otherwise use the 41 | // configured `targetDirectory` value, and fall back to `${module}/target`. 42 | // 43 | // We also allow this to be specified in `local.properties`, not because this is 44 | // something you should ever need to do currently, but we don't want it to ruin anyone's 45 | // day if it turns out we're wrong about that. 46 | val target = 47 | getProperty("rust.cargoTargetDir", "CARGO_TARGET_DIR") 48 | ?: targetDirectory 49 | ?: "${module!!}/target" 50 | 51 | val defaultTargetTriple = getDefaultTargetTriple(project, rustcCommand) 52 | 53 | var cargoOutputDir = File(if (toolchain.target == defaultTargetTriple) { 54 | "${target}/${profile}" 55 | } else { 56 | "${target}/${toolchain.target}/${profile}" 57 | }) 58 | if (!cargoOutputDir.isAbsolute) { 59 | cargoOutputDir = File(project.project.projectDir, cargoOutputDir.path) 60 | } 61 | cargoOutputDir = cargoOutputDir.canonicalFile 62 | 63 | val intoDir = File(buildDir, "rustJniLibs/${toolchain.folder}") 64 | intoDir.mkdirs() 65 | 66 | copy { spec -> 67 | spec.from(cargoOutputDir) 68 | spec.into(intoDir) 69 | 70 | // Need to capture the value to dereference smoothly. 71 | val targetIncludes = targetIncludes 72 | if (targetIncludes != null) { 73 | spec.include(targetIncludes.asIterable()) 74 | } else { 75 | // It's safe to unwrap, since we bailed at configuration time if this is unset. 76 | val libname = libname!! 77 | spec.include("lib${libname}.so") 78 | spec.include("lib${libname}.dylib") 79 | spec.include("${libname}.dll") 80 | } 81 | } 82 | } 83 | } 84 | 85 | inline fun buildProjectForTarget(project: Project, toolchain: Toolchain, ndk: Ndk, cargoExtension: CargoExtension) { 86 | val apiLevel = cargoExtension.apiLevels[toolchain.platform]!! 87 | val defaultTargetTriple = getDefaultTargetTriple(project, cargoExtension.rustcCommand) 88 | 89 | project.exec { spec -> 90 | with(spec) { 91 | standardOutput = System.out 92 | val module = File(cargoExtension.module!!) 93 | if (module.isAbsolute) { 94 | workingDir = module 95 | } else { 96 | workingDir = File(project.project.projectDir, module.path) 97 | } 98 | workingDir = workingDir.canonicalFile 99 | 100 | val theCommandLine = mutableListOf(cargoExtension.cargoCommand) 101 | 102 | if (!cargoExtension.rustupChannel.isEmpty()) { 103 | val hasPlusSign = cargoExtension.rustupChannel.startsWith("+") 104 | val maybePlusSign = if (!hasPlusSign) "+" else "" 105 | 106 | theCommandLine.add(maybePlusSign + cargoExtension.rustupChannel) 107 | } 108 | 109 | theCommandLine.add("build") 110 | 111 | // Respect `verbose` if it is set; otherwise, log if asked to 112 | // with `--info` or `--debug` from the command line. 113 | if (cargoExtension.verbose ?: project.logger.isEnabled(LogLevel.INFO)) { 114 | theCommandLine.add("--verbose") 115 | } 116 | 117 | val features = cargoExtension.featureSpec.features 118 | // We just pass this along to cargo as something space separated... AFAICT 119 | // you're allowed to have featureSpec with spaces in them, but I don't think 120 | // there's a way to specify them in the cargo command line -- rustc accepts 121 | // them if passed in directly with `--cfg`, and cargo will pass them to rustc 122 | // if you use them as default featureSpec. 123 | when (features) { 124 | is Features.All -> { 125 | theCommandLine.add("--all-features") 126 | } 127 | is Features.DefaultAnd -> { 128 | if (!features.featureSet.isEmpty()) { 129 | theCommandLine.add("--features") 130 | theCommandLine.add(features.featureSet.joinToString(" ")) 131 | } 132 | } 133 | is Features.NoDefaultBut -> { 134 | theCommandLine.add("--no-default-features") 135 | if (!features.featureSet.isEmpty()) { 136 | theCommandLine.add("--features") 137 | theCommandLine.add(features.featureSet.joinToString(" ")) 138 | } 139 | } 140 | } 141 | 142 | if (cargoExtension.profile != "debug") { 143 | // Cargo is rigid: it accepts "--release" for release (and 144 | // nothing for dev). This is a cheap way of allowing only 145 | // two values. 146 | theCommandLine.add("--${cargoExtension.profile}") 147 | } 148 | if (toolchain.target != defaultTargetTriple) { 149 | // Only providing --target for the non-default targets means desktop builds 150 | // can share the build cache with `cargo build`/`cargo test`/etc invocations, 151 | // instead of requiring a large amount of redundant work. 152 | theCommandLine.add("--target=${toolchain.target}") 153 | } 154 | 155 | // Target-specific environment configuration, passed through to 156 | // the underlying `cargo build` invocation. 157 | val toolchain_target = toolchain.target.toUpperCase().replace('-', '_') 158 | val prefix = "RUST_ANDROID_GRADLE_TARGET_${toolchain_target}_" 159 | 160 | // For ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_x_KEY=VALUE, set KEY=VALUE. 161 | project.logger.info("Passing through project properties with prefix '${prefix}' (environment variables with prefix 'ORG_GRADLE_PROJECT_${prefix}'") 162 | project.properties.forEach { (key, value) -> 163 | if (key.startsWith(prefix)) { 164 | val realKey = key.substring(prefix.length) 165 | project.logger.debug("Passing through environment variable '${key}' as '${realKey}=${value}'") 166 | environment(realKey, value) 167 | } 168 | } 169 | 170 | // Cross-compiling to Android requires toolchain massaging. 171 | if (toolchain.type != ToolchainType.DESKTOP) { 172 | val ndkPath = ndk.path 173 | val ndkVersionMajor = ndk.versionMajor 174 | 175 | val toolchainDirectory = if (toolchain.type == ToolchainType.ANDROID_PREBUILT) { 176 | environment("CARGO_NDK_MAJOR_VERSION", ndkVersionMajor) 177 | 178 | val hostTag = if (Os.isFamily(Os.FAMILY_WINDOWS)) { 179 | if (Os.isArch("x86_64") || Os.isArch("amd64")) { 180 | "windows-x86_64" 181 | } else { 182 | "windows" 183 | } 184 | } else if (Os.isFamily(Os.FAMILY_MAC)) { 185 | "darwin-x86_64" 186 | } else { 187 | "linux-x86_64" 188 | } 189 | File("$ndkPath/toolchains/llvm/prebuilt", hostTag) 190 | } else { 191 | cargoExtension.toolchainDirectory 192 | } 193 | 194 | val linker_wrapper = 195 | if (System.getProperty("os.name").startsWith("Windows")) { 196 | File(project.rootProject.buildDir, "linker-wrapper/linker-wrapper.bat") 197 | } else { 198 | File(project.rootProject.buildDir, "linker-wrapper/linker-wrapper.sh") 199 | } 200 | environment("CARGO_TARGET_${toolchain_target}_LINKER", linker_wrapper.path) 201 | 202 | val cc = File(toolchainDirectory, "${toolchain.cc(apiLevel)}").path; 203 | val cxx = File(toolchainDirectory, "${toolchain.cxx(apiLevel)}").path; 204 | val ar = File(toolchainDirectory, "${toolchain.ar(apiLevel, ndkVersionMajor)}").path; 205 | 206 | // For build.rs in `cc` consumers: like "CC_i686-linux-android". See 207 | // https://github.com/alexcrichton/cc-rs#external-configuration-via-environment-variables. 208 | environment("CC_${toolchain.target}", cc) 209 | environment("CXX_${toolchain.target}", cxx) 210 | environment("AR_${toolchain.target}", ar) 211 | 212 | // Set CLANG_PATH in the environment, so that bindgen (or anything 213 | // else using clang-sys in a build.rs) works properly, and doesn't 214 | // use host headers and such. 215 | val shouldConfigure = cargoExtension.getFlagProperty( 216 | "rust.autoConfigureClangSys", 217 | "RUST_ANDROID_GRADLE_AUTO_CONFIGURE_CLANG_SYS", 218 | // By default, only do this for non-desktop platforms. If we're 219 | // building for desktop, things should work out of the box. 220 | toolchain.type != ToolchainType.DESKTOP 221 | ) 222 | if (shouldConfigure) { 223 | environment("CLANG_PATH", cc) 224 | } 225 | 226 | // Configure our linker wrapper. 227 | environment("RUST_ANDROID_GRADLE_PYTHON_COMMAND", cargoExtension.pythonCommand) 228 | environment("RUST_ANDROID_GRADLE_LINKER_WRAPPER_PY", 229 | File(project.rootProject.buildDir, "linker-wrapper/linker-wrapper.py").path) 230 | environment("RUST_ANDROID_GRADLE_CC", cc) 231 | if (cargoExtension.generateBuildId) { 232 | environment("RUST_ANDROID_GRADLE_CC_LINK_ARG", "-Wl,--build-id,-soname,lib${cargoExtension.libname!!}.so") 233 | } else { 234 | environment("RUST_ANDROID_GRADLE_CC_LINK_ARG", "-Wl,-soname,lib${cargoExtension.libname!!}.so") 235 | } 236 | } 237 | 238 | cargoExtension.extraCargoBuildArguments?.let { 239 | theCommandLine.addAll(it) 240 | } 241 | 242 | commandLine = theCommandLine 243 | } 244 | if (cargoExtension.exec != null) { 245 | (cargoExtension.exec!!)(spec, toolchain) 246 | } 247 | }.assertNormalExitValue() 248 | } 249 | } 250 | 251 | // This can't be private/internal as it's called from `buildProjectForTarget`. 252 | fun getDefaultTargetTriple(project: Project, rustc: String): String? { 253 | val stdout = ByteArrayOutputStream() 254 | val result = project.exec { spec -> 255 | spec.standardOutput = stdout 256 | spec.commandLine = listOf(rustc, "--version", "--verbose") 257 | } 258 | if (result.exitValue != 0) { 259 | project.logger.warn( 260 | "Failed to get default target triple from rustc (exit code: ${result.exitValue})") 261 | return null 262 | } 263 | val output = stdout.toString() 264 | 265 | // The `rustc --version --verbose` output contains a number of lines like `key: value`. 266 | // We're only interested in `host: `, which corresponds to the default target triple. 267 | val triplePrefix = "host: " 268 | 269 | val triple = output.split("\n") 270 | .find { it.startsWith(triplePrefix) } 271 | ?.let { it.substring(triplePrefix.length).trim() } 272 | 273 | if (triple == null) { 274 | project.logger.warn("Failed to parse `rustc -Vv` output! (Please report a rust-android-gradle bug)") 275 | } else { 276 | project.logger.info("Default rust target triple: $triple") 277 | } 278 | return triple 279 | } 280 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/nishtahir/CargoExtension.kt: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import org.gradle.api.Action 4 | import org.gradle.api.GradleException 5 | import org.gradle.api.Project 6 | import org.gradle.process.ExecSpec 7 | import java.io.File 8 | import java.util.* 9 | 10 | sealed class Features { 11 | class All() : Features() 12 | 13 | data class DefaultAnd(val featureSet: Set) : Features() 14 | 15 | data class NoDefaultBut(val featureSet: Set) : Features() 16 | } 17 | 18 | data class FeatureSpec(var features: Features? = null) { 19 | fun all() { 20 | this.features = Features.All() 21 | } 22 | 23 | fun defaultAnd(featureSet: Array) { 24 | this.features = Features.DefaultAnd(featureSet.toSet()) 25 | } 26 | 27 | fun noDefaultBut(featureSet: Array) { 28 | this.features = Features.NoDefaultBut(featureSet.toSet()) 29 | } 30 | } 31 | 32 | // `CargoExtension` is documented in README.md. 33 | open class CargoExtension { 34 | lateinit var localProperties: Properties 35 | 36 | var module: String? = null 37 | var libname: String? = null 38 | var targets: List? = null 39 | var prebuiltToolchains: Boolean? = null 40 | var profile: String = "debug" 41 | var verbose: Boolean? = null 42 | var targetDirectory: String? = null 43 | var targetIncludes: Array? = null 44 | var apiLevel: Int? = null 45 | var apiLevels: Map = mapOf() 46 | var extraCargoBuildArguments: List? = null 47 | var generateBuildId: Boolean = false 48 | 49 | // It would be nice to use a receiver here, but there are problems interoperating with Groovy 50 | // and Kotlin that are just not worth working out. Another JVM language, yet another dynamic 51 | // invoke solution :( 52 | var exec: ((ExecSpec, Toolchain) -> Unit)? = null 53 | 54 | var featureSpec: FeatureSpec = FeatureSpec() 55 | 56 | fun features(action: Action) { 57 | action.execute(featureSpec) 58 | } 59 | 60 | val toolchainDirectory: File 61 | get() { 62 | // Share a single toolchain directory, if one is configured. Prefer "local.properties" 63 | // to "ANDROID_NDK_TOOLCHAIN_DIR" to "$TMP/rust-android-ndk-toolchains". 64 | val local: String? = localProperties.getProperty("rust.androidNdkToolchainDir") 65 | if (local != null) { 66 | return File(local).absoluteFile 67 | } 68 | 69 | val globalDir: String? = System.getenv("ANDROID_NDK_TOOLCHAIN_DIR") 70 | if (globalDir != null) { 71 | return File(globalDir).absoluteFile 72 | } 73 | 74 | var defaultDir = File(System.getProperty("java.io.tmpdir"), "rust-android-ndk-toolchains") 75 | return defaultDir.absoluteFile 76 | } 77 | 78 | var cargoCommand: String = "" 79 | get() { 80 | return if (!field.isEmpty()) { 81 | field 82 | } else { 83 | getProperty("rust.cargoCommand", "RUST_ANDROID_GRADLE_CARGO_COMMAND") ?: "cargo" 84 | } 85 | } 86 | 87 | var rustupChannel: String = "" 88 | get() { 89 | return if (!field.isEmpty()) { 90 | field 91 | } else { 92 | getProperty("rust.rustupChannel", "RUST_ANDROID_GRADLE_RUSTUP_CHANNEL") ?: "" 93 | } 94 | } 95 | 96 | var pythonCommand: String = "" 97 | get() { 98 | return if (!field.isEmpty()) { 99 | field 100 | } else { 101 | getProperty("rust.pythonCommand", "RUST_ANDROID_GRADLE_PYTHON_COMMAND") ?: "python" 102 | } 103 | } 104 | 105 | // Required so that we can parse the default triple out of `rustc --version --verbose`. Sadly, 106 | // there seems to be no way to get this information out of cargo directly. Failure to locate 107 | // this isn't fatal, however. 108 | var rustcCommand: String = "" 109 | get() { 110 | return if (!field.isEmpty()) { 111 | field 112 | } else { 113 | getProperty("rust.rustcCommand", "RUST_ANDROID_GRADLE_RUSTC_COMMAND") ?: "rustc" 114 | } 115 | } 116 | 117 | fun getFlagProperty(camelCaseName: String, snakeCaseName: String, ifUnset: Boolean): Boolean { 118 | val propVal = getProperty(camelCaseName, snakeCaseName) 119 | if (propVal == "1" || propVal == "true") { 120 | return true 121 | } 122 | if (propVal == "0" || propVal == "false") { 123 | return false 124 | } 125 | if (propVal == null || propVal == "") { 126 | return ifUnset 127 | } 128 | throw GradleException("Illegal value for property \"$camelCaseName\" / \"$snakeCaseName\". Must be 0/1/true/false if set") 129 | } 130 | 131 | internal fun getProperty(camelCaseName: String, snakeCaseName: String): String? { 132 | val local: String? = localProperties.getProperty(camelCaseName) 133 | if (local != null) { 134 | return local 135 | } 136 | val global: String? = System.getenv(snakeCaseName) 137 | if (global != null) { 138 | return global 139 | } 140 | return null 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/nishtahir/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import org.gradle.api.plugins.ExtensionContainer 4 | import kotlin.reflect.KClass 5 | 6 | operator fun ExtensionContainer.get(type: KClass): T = getByType(type.java) 7 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/nishtahir/GenerateLinkerWrapperTask.kt: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import org.gradle.api.tasks.Sync 4 | 5 | open class GenerateLinkerWrapperTask : Sync() { 6 | } 7 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/nishtahir/GenerateToolchainsTask.kt: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import java.io.File 4 | 5 | import com.android.build.gradle.* 6 | import org.gradle.api.DefaultTask 7 | import org.gradle.api.GradleException 8 | import org.gradle.api.Project 9 | import org.gradle.api.tasks.TaskAction 10 | 11 | open class GenerateToolchainsTask : DefaultTask() { 12 | 13 | @TaskAction 14 | @Suppress("unused") 15 | fun generateToolchainTask() { 16 | project.plugins.all { 17 | when (it) { 18 | is AppPlugin -> configureTask(project) 19 | is LibraryPlugin -> configureTask(project) 20 | } 21 | } 22 | } 23 | 24 | inline fun configureTask(project: Project) { 25 | val cargoExtension = project.extensions[CargoExtension::class] 26 | val app = project.extensions[T::class] 27 | val ndkPath = app.ndkDirectory 28 | 29 | // It's safe to unwrap, since we bailed at configuration time if this is unset. 30 | val targets = cargoExtension.targets!! 31 | 32 | toolchains 33 | .filter { it.type == ToolchainType.ANDROID_GENERATED } 34 | .filter { (arch) -> targets.contains(arch) } 35 | .forEach { (arch) -> 36 | // We ensure all architectures have an API level at configuration time 37 | val apiLevel = cargoExtension.apiLevels[arch]!! 38 | 39 | if (arch.endsWith("64") && apiLevel < 21) { 40 | throw GradleException("Can't target 64-bit ${arch} with API level < 21 (${apiLevel})") 41 | } 42 | 43 | // Always regenerate the toolchain, even if it exists 44 | // already. It is fast to do so and fixes any issues 45 | // with partially reclaimed temporary files. 46 | val dir = File(cargoExtension.toolchainDirectory, arch + "-" + apiLevel) 47 | project.exec { spec -> 48 | spec.standardOutput = System.out 49 | spec.errorOutput = System.out 50 | spec.commandLine(cargoExtension.pythonCommand) 51 | spec.args("$ndkPath/build/tools/make_standalone_toolchain.py", 52 | "--arch=$arch", 53 | "--api=$apiLevel", 54 | "--install-dir=${dir}", 55 | "--force") 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/nishtahir/RustAndroidPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import com.android.build.gradle.* 4 | import org.gradle.api.DefaultTask 5 | import org.gradle.api.GradleException 6 | import org.gradle.api.Plugin 7 | import org.gradle.api.Project 8 | import org.gradle.api.file.DuplicatesStrategy 9 | import java.io.File 10 | import java.util.Properties 11 | 12 | const val RUST_TASK_GROUP = "rust" 13 | 14 | enum class ToolchainType { 15 | ANDROID_PREBUILT, 16 | ANDROID_GENERATED, 17 | DESKTOP, 18 | } 19 | 20 | // See https://forge.rust-lang.org/platform-support.html. 21 | val toolchains = listOf( 22 | Toolchain("linux-x86-64", 23 | ToolchainType.DESKTOP, 24 | "x86_64-unknown-linux-gnu", 25 | "", 26 | "", 27 | "desktop/linux-x86-64"), 28 | // This should eventually go away: the darwin-x86-64 target will supersede it. 29 | // https://github.com/mozilla/rust-android-gradle/issues/77 30 | Toolchain("darwin", 31 | ToolchainType.DESKTOP, 32 | "x86_64-apple-darwin", 33 | "", 34 | "", 35 | "desktop/darwin"), 36 | Toolchain("darwin-x86-64", 37 | ToolchainType.DESKTOP, 38 | "x86_64-apple-darwin", 39 | "", 40 | "", 41 | "desktop/darwin-x86-64"), 42 | Toolchain("darwin-aarch64", 43 | ToolchainType.DESKTOP, 44 | "aarch64-apple-darwin", 45 | "", 46 | "", 47 | "desktop/darwin-aarch64"), 48 | Toolchain("win32-x86-64-msvc", 49 | ToolchainType.DESKTOP, 50 | "x86_64-pc-windows-msvc", 51 | "", 52 | "", 53 | "desktop/win32-x86-64"), 54 | Toolchain("win32-x86-64-gnu", 55 | ToolchainType.DESKTOP, 56 | "x86_64-pc-windows-gnu", 57 | "", 58 | "", 59 | "desktop/win32-x86-64"), 60 | Toolchain("arm", 61 | ToolchainType.ANDROID_GENERATED, 62 | "armv7-linux-androideabi", 63 | "arm-linux-androideabi", 64 | "arm-linux-androideabi", 65 | "android/armeabi-v7a"), 66 | Toolchain("arm64", 67 | ToolchainType.ANDROID_GENERATED, 68 | "aarch64-linux-android", 69 | "aarch64-linux-android", 70 | "aarch64-linux-android", 71 | "android/arm64-v8a"), 72 | Toolchain("x86", 73 | ToolchainType.ANDROID_GENERATED, 74 | "i686-linux-android", 75 | "i686-linux-android", 76 | "i686-linux-android", 77 | "android/x86"), 78 | Toolchain("x86_64", 79 | ToolchainType.ANDROID_GENERATED, 80 | "x86_64-linux-android", 81 | "x86_64-linux-android", 82 | "x86_64-linux-android", 83 | "android/x86_64"), 84 | Toolchain("arm", 85 | ToolchainType.ANDROID_PREBUILT, 86 | "armv7-linux-androideabi", // This is correct. "Note: For 32-bit ARM, the compiler is prefixed with 87 | "armv7a-linux-androideabi", // armv7a-linux-androideabi, but the binutils tools are prefixed with 88 | "arm-linux-androideabi", // arm-linux-androideabi. For other architectures, the prefixes are the same 89 | "android/armeabi-v7a"), // for all tools." (Ref: https://developer.android.com/ndk/guides/other_build_systems#overview ) 90 | Toolchain("arm64", 91 | ToolchainType.ANDROID_PREBUILT, 92 | "aarch64-linux-android", 93 | "aarch64-linux-android", 94 | "aarch64-linux-android", 95 | "android/arm64-v8a"), 96 | Toolchain("x86", 97 | ToolchainType.ANDROID_PREBUILT, 98 | "i686-linux-android", 99 | "i686-linux-android", 100 | "i686-linux-android", 101 | "android/x86"), 102 | Toolchain("x86_64", 103 | ToolchainType.ANDROID_PREBUILT, 104 | "x86_64-linux-android", 105 | "x86_64-linux-android", 106 | "x86_64-linux-android", 107 | "android/x86_64") 108 | ) 109 | 110 | data class Ndk(val path: File, val version: String) { 111 | val versionMajor: Int 112 | get() = version.split(".").first().toInt() 113 | } 114 | 115 | data class Toolchain(val platform: String, 116 | val type: ToolchainType, 117 | val target: String, 118 | val compilerTriple: String, 119 | val binutilsTriple: String, 120 | val folder: String) { 121 | fun cc(apiLevel: Int): File = 122 | if (System.getProperty("os.name").startsWith("Windows")) { 123 | if (type == ToolchainType.ANDROID_PREBUILT) { 124 | File("bin", "$compilerTriple$apiLevel-clang.cmd") 125 | } else { 126 | File("$platform-$apiLevel/bin", "$compilerTriple-clang.cmd") 127 | } 128 | } else { 129 | if (type == ToolchainType.ANDROID_PREBUILT) { 130 | File("bin", "$compilerTriple$apiLevel-clang") 131 | } else { 132 | File("$platform-$apiLevel/bin", "$compilerTriple-clang") 133 | } 134 | } 135 | 136 | fun cxx(apiLevel: Int): File = 137 | if (System.getProperty("os.name").startsWith("Windows")) { 138 | if (type == ToolchainType.ANDROID_PREBUILT) { 139 | File("bin", "$compilerTriple$apiLevel-clang++.cmd") 140 | } else { 141 | File("$platform-$apiLevel/bin", "$compilerTriple-clang++.cmd") 142 | } 143 | } else { 144 | if (type == ToolchainType.ANDROID_PREBUILT) { 145 | File("bin", "$compilerTriple$apiLevel-clang++") 146 | } else { 147 | File("$platform-$apiLevel/bin", "$compilerTriple-clang++") 148 | } 149 | } 150 | 151 | fun ar(apiLevel: Int, ndkVersionMajor: Int): File = 152 | if (ndkVersionMajor >= 23) { 153 | File("bin", "llvm-ar") 154 | } else if (type == ToolchainType.ANDROID_PREBUILT) { 155 | File("bin", "$binutilsTriple-ar") 156 | } else { 157 | File("$platform-$apiLevel/bin", "$binutilsTriple-ar") 158 | } 159 | } 160 | 161 | @Suppress("unused") 162 | open class RustAndroidPlugin : Plugin { 163 | internal lateinit var cargoExtension: CargoExtension 164 | 165 | override fun apply(project: Project) { 166 | with(project) { 167 | cargoExtension = extensions.create("cargo", CargoExtension::class.java) 168 | 169 | afterEvaluate { 170 | plugins.all { 171 | when (it) { 172 | is AppPlugin -> configurePlugin(this) 173 | is LibraryPlugin -> configurePlugin(this) 174 | } 175 | } 176 | } 177 | 178 | } 179 | } 180 | 181 | private inline fun configurePlugin(project: Project) = with(project) { 182 | cargoExtension.localProperties = Properties() 183 | 184 | val localPropertiesFile = File(project.rootDir, "local.properties") 185 | if (localPropertiesFile.exists()) { 186 | cargoExtension.localProperties.load(localPropertiesFile.inputStream()) 187 | } 188 | 189 | if (cargoExtension.module == null) { 190 | throw GradleException("module cannot be null") 191 | } 192 | 193 | if (cargoExtension.libname == null) { 194 | throw GradleException("libname cannot be null") 195 | } 196 | 197 | // Allow to set targets, including per-project, in local.properties. 198 | val localTargets: String? = 199 | cargoExtension.localProperties.getProperty("rust.targets.${project.name}") ?: 200 | cargoExtension.localProperties.getProperty("rust.targets") 201 | if (localTargets != null) { 202 | cargoExtension.targets = localTargets.split(',').map { it.trim() } 203 | } 204 | 205 | if (cargoExtension.targets == null) { 206 | throw GradleException("targets cannot be null") 207 | } 208 | 209 | // Ensure that an API level is specified for all targets 210 | val apiLevel = cargoExtension.apiLevel 211 | if (cargoExtension.apiLevels.size > 0) { 212 | if (apiLevel != null) { 213 | throw GradleException("Cannot set both `apiLevel` and `apiLevels`") 214 | } 215 | } else { 216 | val default = if (apiLevel != null) { 217 | apiLevel 218 | } else { 219 | extensions[T::class].defaultConfig.minSdkVersion!!.apiLevel 220 | } 221 | cargoExtension.apiLevels = cargoExtension.targets!!.map { it to default }.toMap() 222 | } 223 | val missingApiLevelTargets = cargoExtension.targets!!.toSet().minus( 224 | cargoExtension.apiLevels.keys) 225 | if (missingApiLevelTargets.size > 0) { 226 | throw GradleException("`apiLevels` missing entries for: $missingApiLevelTargets") 227 | } 228 | 229 | extensions[T::class].apply { 230 | sourceSets.getByName("main").jniLibs.srcDir(File("$buildDir/rustJniLibs/android")) 231 | sourceSets.getByName("test").resources.srcDir(File("$buildDir/rustJniLibs/desktop")) 232 | } 233 | 234 | // Determine the NDK version, if present 235 | val ndk = extensions[T::class].ndkDirectory.let { 236 | val ndkSourceProperties = Properties() 237 | val ndkSourcePropertiesFile = File(it, "source.properties") 238 | if (ndkSourcePropertiesFile.exists()) { 239 | ndkSourceProperties.load(ndkSourcePropertiesFile.inputStream()) 240 | } 241 | val ndkVersion = ndkSourceProperties.getProperty("Pkg.Revision", "0.0") 242 | Ndk(path = it, version = ndkVersion) 243 | } 244 | 245 | // Determine whether to use prebuilt or generated toolchains 246 | val usePrebuilt = 247 | cargoExtension.localProperties.getProperty("rust.prebuiltToolchains")?.equals("true") ?: 248 | cargoExtension.prebuiltToolchains ?: 249 | (ndk.versionMajor >= 19); 250 | 251 | if (usePrebuilt && ndk.versionMajor < 19) { 252 | throw GradleException("usePrebuilt = true requires NDK version 19+") 253 | } 254 | 255 | val generateToolchain = if (!usePrebuilt) { 256 | tasks.maybeCreate("generateToolchains", 257 | GenerateToolchainsTask::class.java).apply { 258 | group = RUST_TASK_GROUP 259 | description = "Generate standard toolchain for given architectures" 260 | } 261 | } else { 262 | null 263 | } 264 | 265 | // Fish linker wrapper scripts from our Java resources. 266 | val generateLinkerWrapper = rootProject.tasks.maybeCreate("generateLinkerWrapper", GenerateLinkerWrapperTask::class.java).apply { 267 | group = RUST_TASK_GROUP 268 | description = "Generate shared linker wrapper script" 269 | } 270 | 271 | generateLinkerWrapper.apply { 272 | // From https://stackoverflow.com/a/320595. 273 | from(rootProject.zipTree(File(RustAndroidPlugin::class.java.protectionDomain.codeSource.location.toURI()).path)) 274 | include("**/linker-wrapper*") 275 | into(File(rootProject.buildDir, "linker-wrapper")) 276 | eachFile { 277 | it.path = it.path.replaceFirst("com/nishtahir", "") 278 | } 279 | fileMode = 493 // 0755 in decimal; Kotlin doesn't have octal literals (!). 280 | includeEmptyDirs = false 281 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 282 | } 283 | 284 | val buildTask = tasks.maybeCreate("cargoBuild", 285 | DefaultTask::class.java).apply { 286 | group = RUST_TASK_GROUP 287 | description = "Build library (all targets)" 288 | } 289 | 290 | cargoExtension.targets!!.forEach { target -> 291 | val theToolchain = toolchains 292 | .filter { 293 | if (usePrebuilt) { 294 | it.type != ToolchainType.ANDROID_GENERATED 295 | } else { 296 | it.type != ToolchainType.ANDROID_PREBUILT 297 | } 298 | } 299 | .find { it.platform == target } 300 | if (theToolchain == null) { 301 | throw GradleException("Target ${target} is not recognized (recognized targets: ${toolchains.map { it.platform }.sorted()}). Check `local.properties` and `build.gradle`.") 302 | } 303 | 304 | val targetBuildTask = tasks.maybeCreate("cargoBuild${target.capitalize()}", 305 | CargoBuildTask::class.java).apply { 306 | group = RUST_TASK_GROUP 307 | description = "Build library ($target)" 308 | toolchain = theToolchain 309 | this.ndk = ndk 310 | } 311 | 312 | if (!usePrebuilt) { 313 | targetBuildTask.dependsOn(generateToolchain!!) 314 | } 315 | targetBuildTask.dependsOn(generateLinkerWrapper) 316 | buildTask.dependsOn(targetBuildTask) 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /plugin/src/main/resources/com/nishtahir/linker-wrapper.bat: -------------------------------------------------------------------------------- 1 | "%RUST_ANDROID_GRADLE_PYTHON_COMMAND%" "%RUST_ANDROID_GRADLE_LINKER_WRAPPER_PY%" %* 2 | -------------------------------------------------------------------------------- /plugin/src/main/resources/com/nishtahir/linker-wrapper.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function, unicode_literals 2 | 3 | import os 4 | import shlex 5 | import subprocess 6 | import sys 7 | 8 | args = [ 9 | os.environ["RUST_ANDROID_GRADLE_CC"], 10 | os.environ["RUST_ANDROID_GRADLE_CC_LINK_ARG"], 11 | ] + sys.argv[1:] 12 | 13 | 14 | def update_in_place(arglist): 15 | # The `gcc` library is not included starting from NDK version 23. 16 | # Work around by using `unwind` replacement. 17 | ndk_major_version = os.environ["CARGO_NDK_MAJOR_VERSION"] 18 | if ndk_major_version.isdigit(): 19 | if 23 <= int(ndk_major_version): 20 | for i, arg in enumerate(arglist): 21 | if arg.startswith("-lgcc"): 22 | # This is one way to preserve line endings. 23 | arglist[i] = "-lunwind" + arg[len("-lgcc") :] 24 | 25 | 26 | update_in_place(args) 27 | 28 | for arg in args: 29 | if arg.startswith("@"): 30 | fileargs = open(arg[1:], "r").read().splitlines(keepends=True) 31 | update_in_place(fileargs) 32 | open(arg[1:], "w").write("".join(fileargs)) 33 | 34 | 35 | # This only appears when the subprocess call fails, but it's helpful then. 36 | printable_cmd = " ".join(shlex.quote(arg) for arg in args) 37 | print(printable_cmd) 38 | 39 | sys.exit(subprocess.call(args)) 40 | -------------------------------------------------------------------------------- /plugin/src/main/resources/com/nishtahir/linker-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Invoke linker-wrapper.py with the correct Python command. 4 | "${RUST_ANDROID_GRADLE_PYTHON_COMMAND}" "${RUST_ANDROID_GRADLE_LINKER_WRAPPER_PY}" "$@" 5 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/com/nishtahir/AbstractTest.groovy: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import org.gradle.testkit.runner.GradleRunner 4 | import org.junit.Rule 5 | import org.junit.rules.TemporaryFolder 6 | import spock.lang.Specification 7 | 8 | class AbstractTest extends Specification { 9 | @Rule TemporaryFolder temporaryFolder 10 | File cacheDir 11 | 12 | def setup() { 13 | cacheDir = temporaryFolder.newFolder() 14 | } 15 | 16 | def withGradleVersion(String gradleVersion) { 17 | GradleRunner.create() 18 | .withGradleVersion(gradleVersion) 19 | .forwardOutput() 20 | .withDebug(false) 21 | } 22 | 23 | File file(String path) { 24 | return new File(temporaryFolder.root, path) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/com/nishtahir/CargoBuildTest.groovy: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import org.gradle.api.GradleException 4 | import org.gradle.testkit.runner.BuildResult 5 | import org.gradle.testkit.runner.TaskOutcome 6 | import spock.lang.Unroll 7 | 8 | import com.nishtahir.Versions 9 | 10 | 11 | @MultiVersionTest 12 | class CargoBuildTest extends AbstractTest { 13 | @Unroll 14 | def "cargoBuild is invoked with #gradleVersion and Android plugin #androidVersion"() { 15 | given: 16 | SimpleAndroidApp.builder(temporaryFolder.root, cacheDir) 17 | .withAndroidVersion(androidVersion) 18 | .withKotlinDisabled() 19 | // TODO: .withCargo(...) 20 | .build() 21 | .writeProject() 22 | 23 | SimpleCargoProject.builder(temporaryFolder.root) 24 | .withTargets(["x86_64"]) 25 | .build() 26 | .writeProject() 27 | 28 | when: 29 | BuildResult buildResult = withGradleVersion(gradleVersion.version) 30 | .withProjectDir(temporaryFolder.root) 31 | .withArguments('cargoBuild', '--info', '--stacktrace') 32 | // .withDebug(true) 33 | .build() 34 | 35 | // To ease debugging. 36 | temporaryFolder.root.eachFileRecurse { 37 | println(it) 38 | } 39 | 40 | then: 41 | buildResult.task(':app:cargoBuild').outcome == TaskOutcome.SUCCESS 42 | buildResult.task(':library:cargoBuild').outcome == TaskOutcome.SUCCESS 43 | new File(temporaryFolder.root, "app/build/rustJniLibs/android/x86_64/librust.so").exists() 44 | new File(temporaryFolder.root, "library/build/rustJniLibs/android/x86_64/librust.so").exists() 45 | 46 | where: 47 | [androidVersion, gradleVersion] << TestVersions.allCandidateTestVersions.entries().collect { [it.key, it.value] } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/com/nishtahir/CargoTargetTest.groovy: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import spock.lang.Unroll 6 | 7 | import com.nishtahir.Versions 8 | 9 | class CargoTargetTest extends AbstractTest { 10 | @Unroll 11 | def "cargoBuild produces #location for target #target"() { 12 | given: 13 | def androidVersion = TestVersions.latestAndroidVersionForCurrentJDK() 14 | 15 | SimpleAndroidApp.builder(temporaryFolder.root, cacheDir) 16 | .withAndroidVersion(androidVersion) 17 | .withKotlinDisabled() 18 | // TODO: .withCargo(...) 19 | .build() 20 | .writeProject() 21 | 22 | SimpleCargoProject.builder(temporaryFolder.root) 23 | .withTargets([target]) 24 | .build() 25 | .writeProject() 26 | 27 | when: 28 | BuildResult buildResult = withGradleVersion(TestVersions.latestSupportedGradleVersionFor(androidVersion).version) 29 | .withProjectDir(temporaryFolder.root) 30 | .withArguments('cargoBuild', '--info', '--stacktrace') 31 | // .withDebug(true) 32 | .build() 33 | 34 | // To ease debugging. 35 | temporaryFolder.root.eachFileRecurse { 36 | println(it) 37 | } 38 | 39 | then: 40 | buildResult.task(':app:cargoBuild').outcome == TaskOutcome.SUCCESS 41 | buildResult.task(':library:cargoBuild').outcome == TaskOutcome.SUCCESS 42 | new File(temporaryFolder.root, "app/build/rustJniLibs/${location}").exists() 43 | new File(temporaryFolder.root, "library/build/rustJniLibs/${location}").exists() 44 | 45 | where: 46 | [target, location] << [ 47 | // Sadly, cross-compiling to macOS targets fails at this time: see 48 | // https://github.com/rust-lang/rust/issues/84984. 49 | // ["darwin", "desktop/darwin/librust.dylib"], 50 | // And so does cross-compiling from macOS to Linux targets. 51 | // ["linux-x86-64", "desktop/linux-x86-64/librust.so"], 52 | ["arm64", "android/arm64-v8a/librust.so"], 53 | ["x86_64", "android/x86_64/librust.so"], 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/com/nishtahir/MultiVersionTest.groovy: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import java.lang.annotation.ElementType 4 | import java.lang.annotation.Inherited 5 | import java.lang.annotation.Retention 6 | import java.lang.annotation.RetentionPolicy 7 | import java.lang.annotation.Target 8 | 9 | /** 10 | * Represents tests that span multiple versions of Android Gradle Plugin and need to be executed 11 | * with multiple versions of the JDK. 12 | */ 13 | @Inherited 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.TYPE) 16 | @interface MultiVersionTest { } 17 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/com/nishtahir/NdkVersionTest.groovy: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import spock.lang.Unroll 6 | 7 | import com.nishtahir.Versions 8 | 9 | class NdkVersionTest extends AbstractTest { 10 | @Unroll 11 | def "cargoBuild works with Android NDK version #ndkVersion"() { 12 | given: 13 | def androidVersion = TestVersions.latestAndroidVersionForCurrentJDK() 14 | def target = "x86_64" 15 | def location = "android/x86_64/librust.so" 16 | 17 | SimpleAndroidApp.builder(temporaryFolder.root, cacheDir) 18 | .withAndroidVersion(androidVersion) 19 | .withNdkVersion(ndkVersion) 20 | .withKotlinDisabled() 21 | // TODO: .withCargo(...) 22 | .build() 23 | .writeProject() 24 | 25 | SimpleCargoProject.builder(temporaryFolder.root) 26 | .withTargets([target]) 27 | .build() 28 | .writeProject() 29 | 30 | // To ease debugging. 31 | temporaryFolder.root.eachFileRecurse { 32 | System.err.println("before> ${it}") 33 | if (it.path.endsWith(".gradle")) { 34 | System.err.println(it.text) 35 | } 36 | } 37 | 38 | when: 39 | BuildResult buildResult = withGradleVersion(TestVersions.latestSupportedGradleVersionFor(androidVersion).version) 40 | .withProjectDir(temporaryFolder.root) 41 | .withArguments('cargoBuild', '--info', '--stacktrace') 42 | // .withDebug(true) 43 | .build() 44 | 45 | // To ease debugging. 46 | temporaryFolder.root.eachFileRecurse { 47 | System.err.println("after> ${it}") 48 | } 49 | 50 | then: 51 | buildResult.task(':app:cargoBuild').outcome == TaskOutcome.SUCCESS 52 | buildResult.task(':library:cargoBuild').outcome == TaskOutcome.SUCCESS 53 | new File(temporaryFolder.root, "app/build/rustJniLibs/${location}").exists() 54 | new File(temporaryFolder.root, "library/build/rustJniLibs/${location}").exists() 55 | 56 | where: 57 | ndkVersion << [ 58 | // Partial list of NDK versions supported by Github Actions, per 59 | // https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2204-Readme.md 60 | "26.3.11579264", 61 | "27.2.12479018", 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/com/nishtahir/SimpleAndroidApp.groovy: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import org.gradle.api.GradleException 4 | import org.gradle.util.VersionNumber 5 | 6 | import static com.nishtahir.Versions.android 7 | 8 | class SimpleAndroidApp { 9 | final File projectDir 10 | private final File cacheDir 11 | final VersionNumber androidVersion 12 | final VersionNumber ndkVersion 13 | final VersionNumber kotlinVersion 14 | private final boolean kotlinEnabled 15 | private final boolean kaptWorkersEnabled 16 | 17 | private SimpleAndroidApp(File projectDir, File cacheDir, VersionNumber androidVersion, VersionNumber ndkVersion, VersionNumber kotlinVersion, boolean kotlinEnabled, boolean kaptWorkersEnabled) { 18 | this.projectDir = projectDir 19 | this.cacheDir = cacheDir 20 | this.androidVersion = androidVersion 21 | this.ndkVersion = ndkVersion 22 | this.kotlinVersion = kotlinVersion 23 | this.kotlinEnabled = kotlinEnabled 24 | this.kaptWorkersEnabled = kaptWorkersEnabled 25 | } 26 | 27 | def writeProject() { 28 | def app = 'app' 29 | def appPackage = 'org.gradle.android.example.app' 30 | def appActivity = 'AppActivity' 31 | 32 | def library = 'library' 33 | def libPackage = 'org.gradle.android.example.library' 34 | def libraryActivity = 'LibraryActivity' 35 | 36 | file("settings.gradle") << """ 37 | buildCache { 38 | local { 39 | directory = "${cacheDir.absolutePath.replace(File.separatorChar, '/' as char)}" 40 | } 41 | } 42 | """.stripIndent() 43 | 44 | file("build.gradle") << """ 45 | buildscript { 46 | repositories { 47 | google() 48 | mavenCentral() 49 | maven { 50 | url = "${System.getProperty("local.repo")}" 51 | } 52 | } 53 | dependencies { 54 | classpath ('com.android.tools.build:gradle:$androidVersion') { force = true } 55 | classpath "org.mozilla.rust-android-gradle:plugin:${Versions.PLUGIN_VERSION}" 56 | ${kotlinPluginDependencyIfEnabled} 57 | } 58 | } 59 | """.stripIndent() 60 | 61 | writeActivity(library, libPackage, libraryActivity) 62 | file("${library}/src/main/AndroidManifest.xml") << """ 63 | 65 | 66 | """.stripIndent() 67 | 68 | writeActivity(app, appPackage, appActivity) 69 | file("${app}/src/main/AndroidManifest.xml") << """ 70 | 72 | 73 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | """.stripIndent() 91 | file("${app}/src/main/res/values/strings.xml") << ''' 92 | 93 | Android Gradle 94 | '''.stripIndent() 95 | 96 | file('settings.gradle') << """ 97 | include ':${app}' 98 | include ':${library}' 99 | """.stripIndent() 100 | 101 | file("${app}/build.gradle") << subprojectConfiguration("com.android.application") << """ 102 | android.defaultConfig.applicationId "org.gradle.android.test.app" 103 | """.stripIndent() << activityDependency() << 104 | """ 105 | dependencies { 106 | implementation project(':${library}') 107 | } 108 | """.stripIndent() 109 | 110 | file("${library}/build.gradle") << subprojectConfiguration("com.android.library") << activityDependency() 111 | 112 | file("gradle.properties") << """ 113 | android.useAndroidX=true 114 | org.gradle.jvmargs=-Xmx2048m 115 | kapt.use.worker.api=${kaptWorkersEnabled} 116 | """.stripIndent() 117 | 118 | configureAndroidSdkHome() 119 | } 120 | 121 | private String getKotlinPluginDependencyIfEnabled() { 122 | return kotlinEnabled ? """ 123 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" 124 | """ : "" 125 | } 126 | 127 | private subprojectConfiguration(String androidPlugin) { 128 | """ 129 | apply plugin: "$androidPlugin" 130 | ${kotlinPluginsIfEnabled} 131 | apply plugin: "org.mozilla.rust-android-gradle.rust-android" 132 | 133 | repositories { 134 | google() 135 | mavenCentral() 136 | } 137 | 138 | dependencies { 139 | ${kotlinDependenciesIfEnabled} 140 | } 141 | 142 | android { 143 | ${maybeNdkVersion} 144 | compileSdkVersion 28 145 | buildToolsVersion "29.0.3" 146 | defaultConfig { 147 | minSdkVersion 28 148 | targetSdkVersion 28 149 | 150 | lintOptions { 151 | checkReleaseBuilds false 152 | } 153 | } 154 | } 155 | """.stripIndent() 156 | } 157 | 158 | private String getMaybeNdkVersion() { 159 | def isAndroid34x = androidVersion >= android("3.4.0") 160 | if (isAndroid34x) { 161 | return """ndkVersion '${ndkVersion}'""" 162 | } else { 163 | return "" 164 | } 165 | } 166 | 167 | private String getKotlinPluginsIfEnabled() { 168 | return kotlinEnabled ? """ 169 | apply plugin: "kotlin-android" 170 | apply plugin: "kotlin-kapt" 171 | """ : "" 172 | } 173 | 174 | private String getKotlinDependenciesIfEnabled() { 175 | return kotlinEnabled ? """ 176 | implementation "org.jetbrains.kotlin:kotlin-stdlib" 177 | """ : "" 178 | } 179 | 180 | private writeActivity(String basedir, String packageName, String className) { 181 | String resourceName = className.toLowerCase() 182 | 183 | file("${basedir}/src/main/java/${packageName.replaceAll('\\.', '/')}/${className}.java") << """ 184 | package ${packageName}; 185 | 186 | import org.joda.time.LocalTime; 187 | 188 | import android.app.Activity; 189 | import android.os.Bundle; 190 | import android.widget.TextView; 191 | 192 | public class ${className} extends Activity { 193 | 194 | @Override 195 | public void onCreate(Bundle savedInstanceState) { 196 | super.onCreate(savedInstanceState); 197 | setContentView(R.layout.${resourceName}_layout); 198 | } 199 | 200 | @Override 201 | public void onStart() { 202 | super.onStart(); 203 | LocalTime currentTime = new LocalTime(); 204 | TextView textView = (TextView) findViewById(R.id.text_view); 205 | textView.setText("The current local time is: " + currentTime); 206 | } 207 | } 208 | """.stripIndent() 209 | 210 | file("${basedir}/src/test/java/${packageName.replaceAll('\\.', '/')}/JavaUserTest.java") << """ 211 | package ${packageName}; 212 | 213 | public class JavaUserTest { 214 | } 215 | """.stripIndent() 216 | 217 | file("${basedir}/src/main/res/layout/${resourceName}_layout.xml") << ''' 218 | 223 | 228 | 229 | '''.stripIndent() 230 | 231 | file("${basedir}/src/main/rs/${resourceName}.rs") << ''' 232 | #pragma version(1) 233 | #pragma rs java_package_name(com.example.myapplication) 234 | 235 | static void addintAccum(int *accum, int val) { 236 | *accum += val; 237 | } 238 | '''.stripIndent() 239 | } 240 | 241 | private static String activityDependency() { 242 | """ 243 | dependencies { 244 | implementation 'joda-time:joda-time:2.7' 245 | } 246 | """.stripIndent() 247 | } 248 | 249 | private void configureAndroidSdkHome() { 250 | file('local.properties').text = "" 251 | def env = System.getenv("ANDROID_HOME") 252 | if (!env) { 253 | def androidSdkHome = new File("${System.getProperty("user.home")}/Library/Android/sdk") 254 | file('local.properties').text += "sdk.dir=${androidSdkHome.absolutePath.replace(File.separatorChar, '/' as char)}" 255 | } 256 | // def env = System.getenv("ANDROID_NDK_HOME") 257 | // if (!env) { 258 | // def androidNdkHome = new File("${System.getProperty("user.home")}/Library/Android/sdk") 259 | // file('local.properties').text += "sdk.dir=${androidSdkHome.absolutePath.replace(File.separatorChar, '/' as char)}" 260 | // } 261 | } 262 | 263 | def file(String path) { 264 | def file = new File(projectDir, path) 265 | file.parentFile.mkdirs() 266 | return file 267 | } 268 | 269 | static Builder builder(File projectDir, File cacheDir) { 270 | return new Builder(projectDir, cacheDir) 271 | } 272 | 273 | static class Builder { 274 | boolean kotlinEnabled = true 275 | boolean kaptWorkersEnabled = true 276 | 277 | VersionNumber androidVersion = Versions.latestAndroidVersion() 278 | VersionNumber ndkVersion = Versions.latestAndroidVersion() >= android("3.4.0") ? VersionNumber.parse("26.3.11579264") : null 279 | 280 | VersionNumber kotlinVersion = VersionNumber.parse("1.3.72") 281 | File projectDir 282 | File cacheDir 283 | 284 | Builder(File projectDir, File cacheDir) { 285 | this.projectDir = projectDir 286 | this.cacheDir = cacheDir 287 | } 288 | 289 | Builder withKotlinDisabled() { 290 | this.kotlinEnabled = false 291 | return this 292 | } 293 | 294 | Builder withKotlinVersion(VersionNumber kotlinVersion) { 295 | this.kotlinVersion = kotlinVersion 296 | return this 297 | } 298 | 299 | Builder withKaptWorkersDisabled() { 300 | this.kaptWorkersEnabled = false 301 | return this 302 | } 303 | 304 | Builder withAndroidVersion(VersionNumber androidVersion) { 305 | this.androidVersion = androidVersion 306 | if (this.androidVersion < android("3.4.0")) { 307 | this.ndkVersion = null 308 | } 309 | return this 310 | } 311 | 312 | Builder withAndroidVersion(String androidVersion) { 313 | return withAndroidVersion(android(androidVersion)) 314 | } 315 | 316 | Builder withNdkVersion(VersionNumber ndkVersion) { 317 | this.ndkVersion = ndkVersion 318 | return this 319 | } 320 | 321 | Builder withNdkVersion(String ndkVersion) { 322 | return withNdkVersion(VersionNumber.parse(ndkVersion)) 323 | } 324 | 325 | Builder withProjectDir(File projectDir) { 326 | this.projectDir = projectDir 327 | return this 328 | } 329 | 330 | Builder withCacheDir(File cacheDir) { 331 | this.cacheDir = cacheDir 332 | return this 333 | } 334 | 335 | SimpleAndroidApp build() { 336 | return new SimpleAndroidApp(projectDir, cacheDir, androidVersion, ndkVersion, kotlinVersion, kotlinEnabled, kaptWorkersEnabled) 337 | } 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/com/nishtahir/SimpleCargoProject.groovy: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | class SimpleCargoProject { 4 | File projectDir 5 | List targets 6 | 7 | SimpleCargoProject(File projectDir, List targets) { 8 | this.projectDir = projectDir 9 | this.targets = targets 10 | } 11 | 12 | static class Builder { 13 | File projectDir 14 | List targets 15 | 16 | Builder(File projectDir) { 17 | this.projectDir = projectDir 18 | } 19 | 20 | def withTargets(targets) { 21 | this.targets = targets 22 | return this 23 | } 24 | 25 | def build() { 26 | if (targets.isEmpty()) { 27 | throw new IllegalStateException("No targets provided") 28 | } 29 | return new SimpleCargoProject(this.projectDir, this.targets) 30 | } 31 | } 32 | 33 | static def builder(File projectDir) { 34 | return new Builder(projectDir) 35 | } 36 | 37 | def file(String path) { 38 | def file = new File(projectDir, path) 39 | file.parentFile.mkdirs() 40 | return file 41 | } 42 | 43 | def writeProject() { 44 | def cargoModuleFile = new File(this.class.classLoader.getResource("rust/Cargo.toml").path).parentFile 45 | def targetDirectoryFile = new File(cargoModuleFile.parentFile, "target") 46 | 47 | // On Windows, path components are backslash-separated. We need to 48 | // express the path as Groovy source, which means backslashes need to be 49 | // escaped. The easiest way is to replace backslashes with forward 50 | // slashes. 51 | def module = cargoModuleFile.path.replace("\\", "/") 52 | def targetDirectory = targetDirectoryFile.path.replace("\\", "/") 53 | 54 | def targetStrings = targets.collect({"\"${it}\"" }).join(", ") 55 | 56 | file('app/build.gradle') << """ 57 | cargo { 58 | module = "${module}" 59 | targetDirectory = "${targetDirectory}" 60 | targets = [${targetStrings}] 61 | libname = "rust" 62 | } 63 | """.stripIndent() 64 | 65 | file('library/build.gradle') << """ 66 | cargo { 67 | module = "${module}" 68 | targetDirectory = "${targetDirectory}" 69 | targets = [${targetStrings}] 70 | libname = "rust" 71 | } 72 | """.stripIndent() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/com/nishtahir/TestVersions.groovy: -------------------------------------------------------------------------------- 1 | package com.nishtahir 2 | 3 | import com.google.common.collect.ImmutableMultimap 4 | import com.google.common.collect.Multimap 5 | import org.gradle.util.GradleVersion 6 | import org.gradle.util.VersionNumber 7 | 8 | 9 | class TestVersions { 10 | static Multimap getAllCandidateTestVersions() { 11 | def testedVersion = System.getProperty('org.gradle.android.testVersion') 12 | if (testedVersion) { 13 | return ImmutableMultimap.copyOf(Versions.SUPPORTED_VERSIONS_MATRIX.entries().findAll {it.key == VersionNumber.parse(testedVersion) }) 14 | } else { 15 | return Versions.SUPPORTED_VERSIONS_MATRIX 16 | } 17 | } 18 | 19 | static VersionNumber latestAndroidVersionForCurrentJDK() { 20 | String currentJDKVersion = System.getProperty("java.version"); 21 | if (currentJDKVersion.startsWith("1.")) { 22 | return allCandidateTestVersions.keySet().findAll {it < VersionNumber.parse("7.0.0-alpha01")}.max() 23 | } 24 | return allCandidateTestVersions.keySet().max() 25 | } 26 | 27 | static GradleVersion latestGradleVersion() { 28 | return allCandidateTestVersions.values().max() 29 | } 30 | 31 | static GradleVersion latestSupportedGradleVersionFor(String androidVersion) { 32 | return latestSupportedGradleVersionFor(VersionNumber.parse(androidVersion)) 33 | } 34 | 35 | static GradleVersion latestSupportedGradleVersionFor(VersionNumber androidVersion) { 36 | return allCandidateTestVersions.asMap().find {it.key.major == androidVersion.major && it.key.minor == androidVersion.minor }?.value?.max() 37 | } 38 | 39 | static VersionNumber getLatestVersionForAndroid(String version) { 40 | VersionNumber versionNumber = VersionNumber.parse(version) 41 | return allCandidateTestVersions.keySet().findAll { it.major == versionNumber.major && it.minor == versionNumber.minor }?.max() 42 | } 43 | 44 | static List getLatestAndroidVersions() { 45 | def minorVersions = allCandidateTestVersions.keySet().collect { "${it.major}.${it.minor}" } 46 | return minorVersions.collect { getLatestVersionForAndroid(it) } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /plugin/src/test/resources/SpockConfig.groovy: -------------------------------------------------------------------------------- 1 | import com.nishtahir.MultiVersionTest 2 | 3 | def testAndroidVersion = System.getProperty('org.gradle.android.testVersion') 4 | 5 | runner { 6 | if (testAndroidVersion) { 7 | include MultiVersionTest 8 | } else { 9 | exclude MultiVersionTest 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /plugin/src/test/resources/rust/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target-dir = "../target" 3 | -------------------------------------------------------------------------------- /plugin/src/test/resources/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust" 3 | version = "0.2.0" 4 | authors = ["Nick Alexander "] 5 | 6 | [dependencies] 7 | jni = "0.5.2" 8 | 9 | [lib] 10 | crate_type = ["staticlib", "dylib"] 11 | 12 | [features] 13 | default = ["foo"] 14 | foo = [] 15 | bar = [] 16 | -------------------------------------------------------------------------------- /plugin/src/test/resources/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate jni; 2 | 3 | use std::ffi::CString; 4 | use std::os::raw::c_char; 5 | 6 | use jni::JNIEnv; 7 | use jni::objects::{JClass, JObject, JValue}; 8 | 9 | pub type Callback = unsafe extern "C" fn(*const c_char) -> (); 10 | 11 | #[no_mangle] 12 | #[allow(non_snake_case)] 13 | pub extern "C" fn invokeCallbackViaJNA(callback: Callback) { 14 | let s = CString::new("Hello from Rust").unwrap(); 15 | unsafe { callback(s.as_ptr()); } 16 | } 17 | 18 | #[no_mangle] 19 | #[allow(non_snake_case)] 20 | pub extern "C" fn Java_com_nishtahir_androidrust_MainActivity_invokeCallbackViaJNI( 21 | env: JNIEnv, 22 | _class: JClass, 23 | callback: JObject 24 | ) { 25 | let s = String::from("Hello from Rust"); 26 | let response = env.new_string(&s) 27 | .expect("Couldn't create java string!"); 28 | env.call_method(callback, "callback", "(Ljava/lang/String;)V", 29 | &[JValue::from(JObject::from(response))]).unwrap(); 30 | } 31 | -------------------------------------------------------------------------------- /samples/app/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.agp_version = '7.0.0' 3 | repositories { 4 | google() 5 | maven { 6 | url "https://plugins.gradle.org/m2/" 7 | } 8 | } 9 | dependencies { 10 | classpath "com.android.tools.build:gradle:$agp_version" 11 | classpath 'org.mozilla.rust-android-gradle:rust-android:+' 12 | } 13 | } 14 | 15 | apply plugin: 'com.android.application' 16 | apply plugin: 'org.mozilla.rust-android-gradle.rust-android' 17 | 18 | android { 19 | compileSdkVersion 27 20 | ndkVersion "26.3.11579264" 21 | 22 | defaultConfig { 23 | applicationId "com.nishtahir.androidrust" 24 | minSdkVersion 21 25 | targetSdkVersion 27 26 | versionCode 1 27 | versionName "1.0" 28 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 29 | } 30 | buildTypes { 31 | release { 32 | minifyEnabled false 33 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 34 | } 35 | } 36 | } 37 | 38 | cargo { 39 | module = "../rust" 40 | targets = ["x86_64", "arm64"] 41 | libname = "rust" 42 | } 43 | 44 | repositories { 45 | google() 46 | mavenCentral() 47 | } 48 | 49 | dependencies { 50 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { 51 | exclude group: 'com.android.support', module: 'support-annotations' 52 | }) 53 | implementation 'com.android.support:appcompat-v7:27.1.1' 54 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 55 | testImplementation 'junit:junit:4.12' 56 | } 57 | 58 | afterEvaluate { 59 | // The `cargoBuild` task isn't available until after evaluation. 60 | android.applicationVariants.all { variant -> 61 | def productFlavor = "" 62 | variant.productFlavors.each { 63 | productFlavor += "${it.name.capitalize()}" 64 | } 65 | def buildType = "${variant.buildType.name.capitalize()}" 66 | tasks["generate${productFlavor}${buildType}Assets"].dependsOn(tasks["cargoBuild"]) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /samples/app/settings.gradle: -------------------------------------------------------------------------------- 1 | includeBuild('../..') { 2 | dependencySubstitution { 3 | // As required. 4 | substitute module('org.mozilla.rust-android-gradle:rust-android') with project(':plugin') 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/app/src/androidTest/java/com/nishtahir/androidrust/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.nishtahir.androidrust; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.nishtahir.androidrust", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/app/src/main/java/com/nishtahir/androidrust/JNICallback.java: -------------------------------------------------------------------------------- 1 | package com.nishtahir.androidrust; 2 | 3 | public interface JNICallback { 4 | public void callback(String string); 5 | } 6 | -------------------------------------------------------------------------------- /samples/app/src/main/java/com/nishtahir/androidrust/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.nishtahir.androidrust; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.util.Log; 6 | import android.widget.TextView; 7 | 8 | public class MainActivity extends AppCompatActivity implements JNICallback { 9 | 10 | private static final String TAG = "MainActivity"; 11 | 12 | // Used to load the 'rust' library on application startup. 13 | static { 14 | System.loadLibrary("rust"); 15 | } 16 | 17 | TextView textView; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_main); 23 | textView = (TextView) findViewById(R.id.sample_text); 24 | 25 | invokeCallbackViaJNI(this); 26 | } 27 | 28 | /** 29 | * A native method that is implemented by the 'rust' native library, 30 | * which is packaged with this application. 31 | */ 32 | public static native void invokeCallbackViaJNI(JNICallback callback); 33 | 34 | @Override 35 | public void callback(String string) { 36 | textView.append("From JNI: " + string + "\n"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /samples/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /samples/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidRust 3 | 4 | -------------------------------------------------------------------------------- /samples/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /samples/library/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.agp_version = '7.0.0' 3 | repositories { 4 | google() 5 | maven { 6 | url "https://plugins.gradle.org/m2/" 7 | } 8 | } 9 | dependencies { 10 | classpath "com.android.tools.build:gradle:$agp_version" 11 | classpath 'org.mozilla.rust-android-gradle:rust-android:+' 12 | } 13 | } 14 | 15 | apply plugin: 'com.android.library' 16 | apply plugin: 'org.mozilla.rust-android-gradle.rust-android' 17 | 18 | android { 19 | compileSdkVersion 27 20 | ndkVersion "26.3.11579264" 21 | 22 | defaultConfig { 23 | minSdkVersion 21 24 | targetSdkVersion 27 25 | versionCode 1 26 | versionName "1.0" 27 | 28 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 29 | } 30 | buildTypes { 31 | release { 32 | minifyEnabled false 33 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 34 | } 35 | } 36 | } 37 | 38 | cargo { 39 | module = "../rust" 40 | targets = ["x86_64", "arm64"] 41 | libname = "rust" 42 | 43 | features { 44 | defaultAnd "foo", "bar" 45 | noDefaultBut("foo", "bar") 46 | all() 47 | } 48 | 49 | exec = { spec, toolchain -> 50 | spec.environment("TEST", "test") 51 | } 52 | } 53 | 54 | repositories { 55 | google() 56 | mavenCentral() 57 | } 58 | 59 | dependencies { 60 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { 61 | exclude group: 'com.android.support', module: 'support-annotations' 62 | }) 63 | implementation 'com.android.support:appcompat-v7:27.1.1' 64 | testImplementation 'junit:junit:4.12' 65 | } 66 | 67 | afterEvaluate { 68 | // The `cargoBuild` task isn't available until after evaluation. 69 | android.libraryVariants.all { variant -> 70 | def productFlavor = "" 71 | variant.productFlavors.each { 72 | productFlavor += "${it.name.capitalize()}" 73 | } 74 | def buildType = "${variant.buildType.name.capitalize()}" 75 | tasks["generate${productFlavor}${buildType}Assets"].dependsOn(tasks["cargoBuild"]) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /samples/library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/nishtahir/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /samples/library/settings.gradle: -------------------------------------------------------------------------------- 1 | includeBuild('../..') { 2 | dependencySubstitution { 3 | // As required. 4 | substitute module('org.mozilla.rust-android-gradle:rust-android') with project(':plugin') 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/library/src/androidTest/java/com/nishtahir/library/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.nishtahir.library; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.nishtahir.library.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /samples/library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | library 3 | 4 | -------------------------------------------------------------------------------- /samples/rust/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /samples/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust" 3 | version = "0.2.0" 4 | authors = ["Nick Alexander "] 5 | 6 | [dependencies] 7 | jni = "0.5.2" 8 | 9 | [lib] 10 | crate_type = ["staticlib", "dylib"] 11 | 12 | [features] 13 | default = ["foo"] 14 | foo = [] 15 | bar = [] 16 | 17 | -------------------------------------------------------------------------------- /samples/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate jni; 2 | 3 | use std::ffi::CString; 4 | use std::os::raw::c_char; 5 | 6 | use jni::JNIEnv; 7 | use jni::objects::{JClass, JObject, JValue}; 8 | 9 | pub type Callback = unsafe extern "C" fn(*const c_char) -> (); 10 | 11 | #[no_mangle] 12 | #[allow(non_snake_case)] 13 | pub extern "C" fn invokeCallbackViaJNA(callback: Callback) { 14 | let s = CString::new("Hello from Rust").unwrap(); 15 | unsafe { callback(s.as_ptr()); } 16 | } 17 | 18 | #[no_mangle] 19 | #[allow(non_snake_case)] 20 | pub extern "C" fn Java_com_nishtahir_androidrust_MainActivity_invokeCallbackViaJNI( 21 | env: JNIEnv, 22 | _class: JClass, 23 | callback: JObject 24 | ) { 25 | let s = String::from("Hello from Rust"); 26 | let response = env.new_string(&s) 27 | .expect("Couldn't create java string!"); 28 | env.call_method(callback, "callback", "(Ljava/lang/String;)V", 29 | &[JValue::from(JObject::from(response))]).unwrap(); 30 | } 31 | -------------------------------------------------------------------------------- /samples/unittest/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.agp_version = '4.0.1' 3 | repositories { 4 | google() 5 | maven { 6 | url "https://plugins.gradle.org/m2/" 7 | } 8 | } 9 | dependencies { 10 | classpath "com.android.tools.build:gradle:${agp_version}" 11 | classpath 'org.mozilla.rust-android-gradle:rust-android:+' 12 | } 13 | } 14 | 15 | apply plugin: 'com.android.application' 16 | apply plugin: 'org.mozilla.rust-android-gradle.rust-android' 17 | 18 | android { 19 | compileSdkVersion 27 20 | defaultConfig { 21 | applicationId "com.nishtahir.androidrust" 22 | minSdkVersion 21 23 | targetSdkVersion 27 24 | versionCode 1 25 | versionName "1.0" 26 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 27 | } 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | ndkVersion "22.1.7171670" 35 | sourceSets { 36 | test.resources.srcDirs += "$buildDir/rustJniLibs/desktop" 37 | } 38 | } 39 | 40 | cargo { 41 | module = "../rust" 42 | targets = ["x86_64", "linux-x86-64"] // "x86", "x86_64", "arm64"] 43 | libname = "rust" 44 | } 45 | 46 | repositories { 47 | google() 48 | } 49 | 50 | 51 | configurations { 52 | // There's an interaction between Gradle's resolution of dependencies with different types 53 | // (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in 54 | // JUnit test runner. The runtime classpath in the built-in JUnit test runner gets the 55 | // dependency from the `implementation`, which is type @aar, and therefore the JNA dependency 56 | // doesn't provide the JNI dispatch libraries in the correct Java resource directories. I think 57 | // what's happening is that @aar type in `implementation` resolves to the @jar type in 58 | // `testImplementation`, and that it wins the dependency resolution battle. 59 | // 60 | // A workaround is to add a new configuration which depends on the @jar type and to reference 61 | // the underlying JAR file directly in `testImplementation`. This JAR file doesn't resolve to 62 | // the @aar type in `implementation`. This works when invoked via `gradle`, but also sets the 63 | // correct runtime classpath when invoked with Android Studio's built-in JUnit test runner. 64 | // Success! 65 | jnaForTest 66 | } 67 | 68 | dependencies { 69 | jnaForTest "net.java.dev.jna:jna:5.6.0@jar" 70 | implementation "net.java.dev.jna:jna:5.6.0@aar" 71 | 72 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { 73 | exclude group: 'com.android.support', module: 'support-annotations' 74 | }) 75 | implementation 'com.android.support:appcompat-v7:27.1.1' 76 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 77 | testImplementation 'junit:junit:4.12' 78 | 79 | // For reasons unknown, resolving the jnaForTest configuration directly 80 | // trips a nasty issue with the Android-Gradle plugin 3.2.1, like `Cannot 81 | // change attributes of configuration ':PROJECT:kapt' after it has been 82 | // resolved`. I think that the configuration is being made a 83 | // super-configuration of the testImplementation and then the `.files` is 84 | // causing it to be resolved. Cloning first dissociates the configuration, 85 | // avoiding other configurations from being resolved. Tricky! 86 | testImplementation files(configurations.jnaForTest.copyRecursive().files) 87 | // testImplementation "androidx.test.ext:junit:$versions.androidx_junit" 88 | testImplementation "org.robolectric:robolectric:4.2.1" 89 | } 90 | 91 | afterEvaluate { 92 | // The `cargoBuild` task isn't available until after evaluation. 93 | android.applicationVariants.all { variant -> 94 | def productFlavor = "" 95 | variant.productFlavors.each { 96 | productFlavor += "${it.name.capitalize()}" 97 | } 98 | def buildType = "${variant.buildType.name.capitalize()}" 99 | tasks["generate${productFlavor}${buildType}Assets"].dependsOn(tasks["cargoBuild"]) 100 | 101 | // Don't merge the jni lib folders until after the Rust libraries have been built. 102 | tasks["merge${productFlavor}${buildType}JniLibFolders"].dependsOn(tasks["cargoBuild"]) 103 | 104 | // For unit tests. 105 | tasks["process${productFlavor}${buildType}UnitTestJavaRes"].dependsOn(tasks["cargoBuild"]) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /samples/unittest/gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | -------------------------------------------------------------------------------- /samples/unittest/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/unittest/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /samples/unittest/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /samples/unittest/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /samples/unittest/settings.gradle: -------------------------------------------------------------------------------- 1 | includeBuild('../..') { 2 | dependencySubstitution { 3 | // As required. 4 | substitute module('org.mozilla.rust-android-gradle:rust-android') with project(':plugin') 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/unittest/src/androidTest/java/com/nishtahir/androidrust/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.nishtahir.androidrust; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.nishtahir.androidrust", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/unittest/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/unittest/src/main/java/com/nishtahir/androidrust/JNACallback.java: -------------------------------------------------------------------------------- 1 | package com.nishtahir.androidrust; 2 | 3 | import com.sun.jna.Callback; 4 | 5 | public interface JNACallback extends Callback { 6 | public void invoke(String string); 7 | } 8 | -------------------------------------------------------------------------------- /samples/unittest/src/main/java/com/nishtahir/androidrust/JNICallback.java: -------------------------------------------------------------------------------- 1 | package com.nishtahir.androidrust; 2 | 3 | public interface JNICallback { 4 | public void callback(String string); 5 | } 6 | -------------------------------------------------------------------------------- /samples/unittest/src/main/java/com/nishtahir/androidrust/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.nishtahir.androidrust; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.util.Log; 6 | import android.widget.TextView; 7 | 8 | import com.sun.jna.Library; 9 | import com.sun.jna.Native; 10 | import com.sun.jna.NativeLibrary; 11 | 12 | public class MainActivity extends AppCompatActivity implements JNACallback, JNICallback { 13 | 14 | private static final String TAG = "MainActivity"; 15 | 16 | public interface RustLibrary extends Library { 17 | RustLibrary INSTANCE = (RustLibrary) 18 | Native.load("rust", RustLibrary.class); 19 | 20 | int invokeCallbackViaJNA(JNACallback callback); 21 | } 22 | 23 | static { 24 | // On Android, this can be just: 25 | // System.loadLibrary("rust"); 26 | // But when running as a unit test, we need to fish the libraries from 27 | // Java resources and configure the classpath. We use JNA for that. 28 | NativeLibrary LIBRARY = NativeLibrary.getInstance("rust"); 29 | System.load(LIBRARY.getFile().getPath()); 30 | } 31 | 32 | TextView textView; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | textView = (TextView) findViewById(R.id.sample_text); 39 | 40 | invokeCallbackViaJNA(this); 41 | invokeCallbackViaJNI(this); 42 | } 43 | 44 | /** 45 | * A native method that is implemented by the 'rust' native library, 46 | * which is packaged with this application. 47 | */ 48 | public static native void invokeCallbackViaJNI(JNICallback callback); 49 | 50 | public static void invokeCallbackViaJNA(JNACallback callback) { 51 | RustLibrary.INSTANCE.invokeCallbackViaJNA(callback); 52 | } 53 | 54 | @Override 55 | public void invoke(String string) { 56 | textView.append("From JNA: " + string + "\n"); 57 | } 58 | 59 | @Override 60 | public void callback(String string) { 61 | textView.append("From JNI: " + string + "\n"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /samples/unittest/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/unittest/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/unittest/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/unittest/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/unittest/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/unittest/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/unittest/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/unittest/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/unittest/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/unittest/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/unittest/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/rust-android-gradle/898d9fd4fc4e2b25ad1c84cbed38962d6ad5e0b2/samples/unittest/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/unittest/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /samples/unittest/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidRust 3 | 4 | -------------------------------------------------------------------------------- /samples/unittest/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/unittest/src/test/java/com/nishtahir/androidrust/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package com.nishtahir.androidrust; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.robolectric.RobolectricTestRunner; 10 | import org.junit.Assert; 11 | 12 | @RunWith(RobolectricTestRunner.class) 13 | public class ExampleUnitTest implements JNACallback, JNICallback { 14 | private String callbackValue; 15 | 16 | @Override 17 | public void invoke(String value) { 18 | // Here's the JNA version. 19 | callbackValue = "From JNA: " + value; 20 | } 21 | 22 | @Override 23 | public void callback(String value) { 24 | // Here's the JNI version. 25 | callbackValue = "From JNI: " + value; 26 | } 27 | 28 | @Test 29 | public void testViaJNI() { 30 | MainActivity.invokeCallbackViaJNI(this); 31 | Assert.assertEquals("From JNI: Hello from Rust", callbackValue); 32 | } 33 | 34 | @Test 35 | public void testViaJNA() { 36 | MainActivity.invokeCallbackViaJNA(this); 37 | Assert.assertEquals("From JNA: Hello from Rust", callbackValue); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'rust-android' 2 | include ':plugin' 3 | -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | version=0.9.6 2 | --------------------------------------------------------------------------------