From f57ba0672e20f0d244fd9635858e8fe92c9e03e4 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 11 Aug 2026 15:04:01 +0200 Subject: [PATCH 01/10] Add sbom generator --- .github/actions/android/action.yml | 87 ------- .github/actions/linux/action.yml | 33 --- .github/actions/macos/action.yml | 24 -- .github/actions/wasm/action.yml | 19 -- .github/actions/windows/action.yml | 24 -- .github/actions/xcframework/action.yml | 50 ---- .github/workflows/build.yml | 310 +++++++++++++++++++++++++ .github/workflows/release.yml | 97 ++------ .github/workflows/tests.yml | 79 +------ dart/pubspec.lock | 2 +- dart/pubspec.yaml | 1 + dart/tool/generate_sbom.dart | 159 +++++++++++++ 12 files changed, 497 insertions(+), 388 deletions(-) delete mode 100644 .github/actions/android/action.yml delete mode 100644 .github/actions/linux/action.yml delete mode 100644 .github/actions/macos/action.yml delete mode 100644 .github/actions/wasm/action.yml delete mode 100644 .github/actions/windows/action.yml delete mode 100644 .github/actions/xcframework/action.yml create mode 100644 .github/workflows/build.yml create mode 100644 dart/tool/generate_sbom.dart diff --git a/.github/actions/android/action.yml b/.github/actions/android/action.yml deleted file mode 100644 index 5f28641..0000000 --- a/.github/actions/android/action.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: "Build Android library" -description: "Create artifact for Android library" -inputs: - sign-publication: - description: "Whether to sign the built library" - default: '1' - gpg-key: - required: false - description: "The GPG key to use when signing the publication" - gpg-password: - required: false - description: "Password for the GPG key." - -runs: - using: "composite" - steps: - - uses: actions/setup-java@v5 - with: - distribution: "temurin" - java-version: "25" - - - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v4 - - - name: Setup - shell: bash - run: | - rustup toolchain install nightly-2026-04-10-x86_64-unknown-linux-gnu - rustup component add rust-src --toolchain nightly-2026-04-10-x86_64-unknown-linux-gnu - rustup target add \ - aarch64-linux-android \ - armv7-linux-androideabi \ - x86_64-linux-android \ - i686-linux-android - cargo install cargo-ndk - - - name: Build signed library - shell: bash - if: ${{ inputs.sign-publication == '1' }} - run: | - cd android - ./gradlew build zipPublication -PgpgKey=${INPUTS_GPG_KEY} -PgpgPassword=${INPUTS_GPG_PASSWORD} - ls -lh build/outputs/aar - find build/repository - env: - INPUTS_GPG_KEY: ${{ inputs.gpg-key }} - INPUTS_GPG_PASSWORD: ${{ inputs.gpg-password }} - - - name: Build library without signing - shell: bash - if: ${{ inputs.sign-publication == '0' }} - run: | - cd android - ./gradlew build zipPublication -PsignPublication=0 - ls -lh build/outputs/aar - find build/repository - - - name: Upload binary - uses: actions/upload-artifact@v4 - with: - name: android-library - retention-days: 14 - compression-level: 0 # We're uploading a zip, no need to compress again - path: android/build/distributions/powersync_android.zip - if-no-files-found: error - - - name: Build individual libraries for Dart SDK - shell: bash - run: | - cd android - ./gradlew buildRustStandalone - - - name: Copy individual libraries - shell: bash - run: | - cp android/build/standalone/arm64-v8a/libpowersync.so libpowersync_aarch64.android.so - cp android/build/standalone/armeabi-v7a/libpowersync.so libpowersync_armv7.android.so - cp android/build/standalone/x86/libpowersync.so libpowersync_x86.android.so - cp android/build/standalone/x86_64/libpowersync.so libpowersync_x64.android.so - - - name: Upload individual libraries - uses: actions/upload-artifact@v4 - with: - name: android-static - retention-days: 14 - path: | - *.so diff --git a/.github/actions/linux/action.yml b/.github/actions/linux/action.yml deleted file mode 100644 index 67da9e3..0000000 --- a/.github/actions/linux/action.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: "Build Linux libraries" -description: "Create artifact for Linux libraries" - -runs: - using: "composite" - steps: - - name: Install Rust Nightly - shell: bash - run: - rustup install --target aarch64-unknown-linux-gnu,x86_64-unknown-linux-gnu,i686-unknown-linux-gnu,riscv64gc-unknown-linux-gnu,armv7-unknown-linux-gnueabihf - - - name: Install cross-compiling GCC - shell: bash - run: | - sudo apt update - sudo apt install -y gcc-aarch64-linux-gnu gcc-riscv64-linux-gnu gcc-arm-linux-gnueabihf gcc-i686-linux-gnu - - - name: Build binaries - shell: bash - run: | - ./tool/build_linux.sh x64 - ./tool/build_linux.sh aarch64 - ./tool/build_linux.sh x86 - ./tool/build_linux.sh armv7 - ./tool/build_linux.sh riscv64gc - - - uses: actions/upload-artifact@v4 - with: - name: linux-library - retention-days: 14 - path: | - *.so - *.linux.a diff --git a/.github/actions/macos/action.yml b/.github/actions/macos/action.yml deleted file mode 100644 index 4e07d7c..0000000 --- a/.github/actions/macos/action.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: "Build macoS libraries" -description: "Create artifact for macOS libraries" - -runs: - using: "composite" - steps: - - name: Install Rust Nightly - shell: bash - run: - rustup install --target x86_64-apple-darwin,aarch64-apple-darwin - - - name: Build binaries - shell: bash - run: | - ./tool/build_macos.sh x64 - ./tool/build_macos.sh aarch64 - - - uses: actions/upload-artifact@v4 - with: - name: macos-library - retention-days: 14 - path: | - *.dylib - *.a diff --git a/.github/actions/wasm/action.yml b/.github/actions/wasm/action.yml deleted file mode 100644 index 408f1a8..0000000 --- a/.github/actions/wasm/action.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: "Build wasm libraries" -description: "Create artifact for wasm libraries" - -runs: - using: "composite" - steps: - - name: Install Rust Nightly - shell: bash - run: rustup install - - - name: Build WASM - shell: bash - run: ./tool/build_wasm.sh - - - uses: actions/upload-artifact@v4 - with: - name: wasm-library - retention-days: 14 - path: libpowersync-wasm.a diff --git a/.github/actions/windows/action.yml b/.github/actions/windows/action.yml deleted file mode 100644 index 4ecf5f1..0000000 --- a/.github/actions/windows/action.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: "Build Windows libraries" -description: "Create artifact for Windows libraries" - -runs: - using: "composite" - steps: - - name: Install Rust Nightly - shell: powershell - run: rustup install --target x86_64-pc-windows-msvc,aarch64-pc-windows-msvc,i686-pc-windows-msvc - - - name: Build binaries - shell: bash - run: | - ./tool/build_windows.sh x64 - ./tool/build_windows.sh aarch64 - ./tool/build_windows.sh x86 - - - uses: actions/upload-artifact@v4 - with: - name: windows-library - retention-days: 14 - path: | - *.dll - *.lib diff --git a/.github/actions/xcframework/action.yml b/.github/actions/xcframework/action.yml deleted file mode 100644 index 2120784..0000000 --- a/.github/actions/xcframework/action.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: "Build xcframework" -description: "Create artifact with XCFramework for apple targets" - -runs: - using: "composite" - steps: - - name: Setup - shell: bash - run: | - rustup toolchain install nightly-2026-04-10-aarch64-apple-darwin - rustup component add rust-src --toolchain nightly-2026-04-10-aarch64-apple-darwin - rustup target add \ - x86_64-apple-darwin \ - aarch64-apple-darwin \ - aarch64-apple-ios \ - aarch64-apple-ios-sim \ - x86_64-apple-ios - - - name: setup-cocoapods - uses: maxim-lobanov/setup-cocoapods@8e97e1e98e6ccf42564fdf5622c8feec74199377 # v1.4.0 - with: - version: 1.16.2 - - - name: Set up XCode - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 - with: - xcode-version: latest-stable - - - name: List simulators - shell: bash - run: | - xcrun xctrace list devices - - - name: Build iOS & macOS xcframework - shell: bash - run: | - ./tool/build_xcframework.sh - - - name: Lint pod - shell: bash - run: | - pod lib lint - - - uses: actions/upload-artifact@v4 - with: - name: xcframework - retention-days: 14 - compression-level: 0 # We're uploading a zip archive, no need to compress agan - path: | - powersync-sqlite-core.xcframework.zip diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..670f92a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,310 @@ +name: Compile core extension binaries + +on: + workflow_call: + inputs: + attest: + description: "Whether to generate a signed attestation for compiled artifacts" + type: boolean + default: false + sign-publication: + description: "Whether to sign the built Android library" + default: false + type: boolean + gpg-key: + required: false + description: "The GPG key to use when signing the publication" + type: string + gpg-password: + required: false + description: "Password for the GPG key." + type: string + outputs: + libs_linux: + description: "Artifact ID of the uploaded linux-library artifact" + value: ${{ jobs.libs_linux.outputs.artifact_id }} + libs_macos: + description: "Artifact ID of the uploaded macos-library artifact" + value: ${{ jobs.libs_macos.outputs.artifact_id }} + libs_windows: + description: "Artifact ID of the uploaded windows-library artifact" + value: ${{ jobs.libs_windows.outputs.artifact_id }} + libs_android: + description: "Artifact ID of the uploaded android-library artifact" + value: ${{ jobs.libs_android.outputs.artifact_id }} + libs_android_static: + description: "Artifact ID of the uploaded android-static artifact" + value: ${{ jobs.libs_android.outputs.static_artifact_id }} + libs_wasm: + description: "Artifact ID of the uploaded wasm-library artifact" + value: ${{ jobs.libs_wasm.outputs.artifact_id }} + libs_xcframework: + description: "Artifact ID of the uploaded xcframework artifact" + value: ${{ jobs.libs_xcframework.outputs.artifact_id }} + +permissions: {} + +jobs: + libs_linux: + name: Building Linux libraries + runs-on: ubuntu-latest + outputs: + artifact_id: ${{ steps.upload.outputs.artifact-id }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Install Rust Nightly + shell: bash + run: + rustup install --target aarch64-unknown-linux-gnu,x86_64-unknown-linux-gnu,i686-unknown-linux-gnu,riscv64gc-unknown-linux-gnu,armv7-unknown-linux-gnueabihf + + - name: Install cross-compiling GCC + shell: bash + run: | + sudo apt update + sudo apt install -y gcc-aarch64-linux-gnu gcc-riscv64-linux-gnu gcc-arm-linux-gnueabihf gcc-i686-linux-gnu + + - name: Build binaries + shell: bash + run: | + ./tool/build_linux.sh x64 + ./tool/build_linux.sh aarch64 + ./tool/build_linux.sh x86 + ./tool/build_linux.sh armv7 + ./tool/build_linux.sh riscv64gc + + - uses: actions/upload-artifact@v4 + id: upload + with: + name: linux-library + retention-days: 14 + path: | + *.so + *.linux.a + + libs_macos: + name: Building macOS libraries + runs-on: macos-latest + outputs: + artifact_id: ${{ steps.upload.outputs.artifact-id }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Install Rust Nightly + shell: bash + run: + rustup install --target x86_64-apple-darwin,aarch64-apple-darwin + + - name: Build binaries + shell: bash + run: | + ./tool/build_macos.sh x64 + ./tool/build_macos.sh aarch64 + + - uses: actions/upload-artifact@v4 + id: upload + with: + name: macos-library + retention-days: 14 + path: | + *.dylib + *.a + + libs_windows: + name: Building Windows libraries + runs-on: windows-latest + outputs: + artifact_id: ${{ steps.upload.outputs.artifact-id }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Install Rust Nightly + shell: powershell + run: rustup install --target x86_64-pc-windows-msvc,aarch64-pc-windows-msvc,i686-pc-windows-msvc + + - name: Build binaries + shell: bash + run: | + ./tool/build_windows.sh x64 + ./tool/build_windows.sh aarch64 + ./tool/build_windows.sh x86 + + - uses: actions/upload-artifact@v4 + id: upload + with: + name: windows-library + retention-days: 14 + path: | + *.dll + *.lib + + libs_android: + name: Building Android libraries + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) + runs-on: ubuntu-latest + outputs: + artifact_id: ${{ steps.upload.outputs.artifact-id }} + static_artifact_id: ${{ steps.upload_static.outputs.artifact-id }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-java@v5 + with: + distribution: "temurin" + java-version: "25" + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + + - name: Setup + shell: bash + run: | + rustup toolchain install nightly-2026-04-10-x86_64-unknown-linux-gnu + rustup component add rust-src --toolchain nightly-2026-04-10-x86_64-unknown-linux-gnu + rustup target add \ + aarch64-linux-android \ + armv7-linux-androideabi \ + x86_64-linux-android \ + i686-linux-android + cargo install cargo-ndk + + - name: Build signed library + shell: bash + if: ${{ inputs.sign-publication == '1' }} + run: | + cd android + ./gradlew build zipPublication -PgpgKey=${INPUTS_GPG_KEY} -PgpgPassword=${INPUTS_GPG_PASSWORD} + ls -lh build/outputs/aar + find build/repository + env: + INPUTS_GPG_KEY: ${{ inputs.gpg-key }} + INPUTS_GPG_PASSWORD: ${{ inputs.gpg-password }} + + - name: Build library without signing + shell: bash + if: ${{ inputs.sign-publication == '0' }} + run: | + cd android + ./gradlew build zipPublication -PsignPublication=0 + ls -lh build/outputs/aar + find build/repository + + - name: Upload binary + uses: actions/upload-artifact@v4 + id: upload + with: + name: android-library + retention-days: 14 + compression-level: 0 # We're uploading a zip, no need to compress again + path: android/build/distributions/powersync_android.zip + if-no-files-found: error + + - name: Build individual libraries for Dart SDK + shell: bash + run: | + cd android + ./gradlew buildRustStandalone + + - name: Copy individual libraries + shell: bash + run: | + cp android/build/standalone/arm64-v8a/libpowersync.so libpowersync_aarch64.android.so + cp android/build/standalone/armeabi-v7a/libpowersync.so libpowersync_armv7.android.so + cp android/build/standalone/x86/libpowersync.so libpowersync_x86.android.so + cp android/build/standalone/x86_64/libpowersync.so libpowersync_x64.android.so + + - name: Upload individual libraries + uses: actions/upload-artifact@v4 + id: upload_static + with: + name: android-static + retention-days: 14 + path: | + *.so + + libs_wasm: + name: Basic WASM build + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) + runs-on: ubuntu-latest + outputs: + artifact_id: ${{ steps.upload.outputs.artifact-id }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Install Rust Nightly + shell: bash + run: rustup install + + - name: Build WASM + shell: bash + run: ./tool/build_wasm.sh + + - uses: actions/upload-artifact@v4 + id: upload + with: + name: wasm-library + retention-days: 14 + path: libpowersync-wasm.a + + libs_xcframework: + name: Build XCFramework + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) + runs-on: macos-latest + outputs: + artifact_id: ${{ steps.upload.outputs.artifact-id }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Setup + shell: bash + run: | + rustup toolchain install nightly-2026-04-10-aarch64-apple-darwin + rustup component add rust-src --toolchain nightly-2026-04-10-aarch64-apple-darwin + rustup target add \ + x86_64-apple-darwin \ + aarch64-apple-darwin \ + aarch64-apple-ios \ + aarch64-apple-ios-sim \ + x86_64-apple-ios + + - name: setup-cocoapods + uses: maxim-lobanov/setup-cocoapods@8e97e1e98e6ccf42564fdf5622c8feec74199377 # v1.4.0 + with: + version: 1.16.2 + + - name: Set up XCode + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 + with: + xcode-version: latest-stable + + - name: List simulators + shell: bash + run: | + xcrun xctrace list devices + + - name: Build iOS & macOS xcframework + shell: bash + run: | + ./tool/build_xcframework.sh + + - name: Lint pod + shell: bash + run: | + pod lib lint + + - uses: actions/upload-artifact@v4 + id: upload + with: + name: xcframework + retention-days: 14 + compression-level: 0 # We're uploading a zip archive, no need to compress agan + path: | + powersync-sqlite-core.xcframework.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f209abc..93e08e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,73 +35,18 @@ jobs: body="Release $tag" gh release create --draft "$tag" --title "$tag" --notes "$body" - libs_linux: - name: Building Linux libraries - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Build Linux - uses: ./.github/actions/linux - - libs_macos: - name: Building macOS libraries - runs-on: macos-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Build macOS - uses: ./.github/actions/macos - - libs_windows: - name: Building Windows libraries - runs-on: windows-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Build Windows - uses: ./.github/actions/windows - - libs_android: - name: Building Android libraries - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Build Android - uses: ./.github/actions/android - with: - gpg-key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg-password: ${{ secrets.GPG_PASSWORD }} - - libs_wasm: - name: Basic WASM build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Build wasm - uses: ./.github/actions/wasm - - libs_xcframework: - name: Build XCFramework - runs-on: macos-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Build XCFramework - uses: ./.github/actions/xcframework + build_libs: + name: Build libraries + uses: ./.github/workflows/build.yml + with: + attest: true + sign-publication: true + gpg-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-password: ${{ secrets.GPG_PASSWORD }} publish_android: name: Publish Android - needs: [ draft_release, libs_android ] + needs: [ draft_release, build_libs ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -110,7 +55,7 @@ jobs: - uses: actions/download-artifact@v8 with: - name: android-library + artifact-ids: ${{ needs.build_libs.outputs.libs_android }} - name: Publish to Maven Central env: @@ -123,7 +68,7 @@ jobs: publish_ios_pod_and_spm_package: name: Publish iOS - needs: [ draft_release, libs_xcframework ] + needs: [ draft_release, build_libs ] runs-on: macos-latest permissions: contents: write # Needed to upload the XCFramework to the draft release. @@ -135,7 +80,7 @@ jobs: - name: Download libs uses: actions/download-artifact@v8 with: - name: xcframework + artifact-ids: ${{ needs.build_libs.outputs.libs_xcframework }} - name: Extract xcframework run: unzip powersync-sqlite-core.xcframework.zip "powersync-sqlite-core.xcframework/*" @@ -185,7 +130,7 @@ jobs: publish_desktop: name: Publish Desktop libraries - needs: [ draft_release, libs_linux, libs_macos, libs_windows ] + needs: [ draft_release, build_libs ] runs-on: ubuntu-latest permissions: contents: write # Needed to upload binaries to release. @@ -198,23 +143,19 @@ jobs: - name: Download Linux libraries uses: actions/download-artifact@v8 with: - name: linux-library + artifact-ids: ${{ needs.build_libs.outputs.libs_linux }} - name: Download macOS libraries uses: actions/download-artifact@v8 with: - name: macos-library - - name: Download Windows libraries - uses: actions/download-artifact@v8 - with: - name: windows-library + artifact-ids: ${{ needs.build_libs.outputs.libs_macos }} - name: Download Windows libraries uses: actions/download-artifact@v8 with: - name: windows-library + artifact-ids: ${{ needs.build_libs.outputs.libs_windows }} - name: Download static Android libraries uses: actions/download-artifact@v8 with: - name: android-static + artifact-ids: ${{ needs.build_libs.outputs.libs_android_static }} - name: Create archive of static libs for Kotlin run: zip static_libs.zip *.lib *.a @@ -235,7 +176,7 @@ jobs: publish_wasm: name: Publish WASM builds - needs: [ draft_release, libs_wasm ] + needs: [ draft_release, build_libs ] runs-on: ubuntu-latest permissions: contents: write # Needed to upload binaries to release. @@ -247,7 +188,7 @@ jobs: - name: Download wasm bundle uses: actions/download-artifact@v8 with: - name: wasm-library + artifact-ids: ${{ needs.build_libs.outputs.libs_wasm }} - name: Upload wasm binaries env: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8aea92a..6aa168e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,75 +23,10 @@ jobs: with: persona: pedantic - libs_linux: - name: Building Linux libraries - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Build Linux libraries - uses: ./.github/actions/linux - - libs_macos: - name: Building macOS libraries - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) - runs-on: macos-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Build macOS - uses: ./.github/actions/macos - - libs_windows: - name: Building Windows libraries - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) - runs-on: windows-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Build Windows - uses: ./.github/actions/windows - - libs_android: - name: Building Android libraries + build_libs: + name: Build libraries if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Build Android - uses: ./.github/actions/android - with: - sign-publication: '0' - - libs_wasm: - name: Basic WASM build - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Build wasm - uses: ./.github/actions/wasm - - libs_xcframework: - name: Build XCFramework - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) - runs-on: macos-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Build XCFramework - uses: ./.github/actions/xcframework + uses: ./.github/workflows/build.yml rust_unit_tests: name: Rust unit tests on ${{ matrix.os }} @@ -140,7 +75,7 @@ jobs: name: Testing on ${{ matrix.os }} if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) runs-on: ${{ matrix.os }} - needs: [libs_linux, libs_macos, libs_windows] + needs: [build_libs] strategy: fail-fast: false matrix: @@ -167,17 +102,17 @@ jobs: - name: Download libs uses: actions/download-artifact@v8 with: - name: linux-library + artifact-ids: ${{ needs.build_libs.outputs.libs_linux }} path: dart/assets - name: Download libs uses: actions/download-artifact@v8 with: - name: macos-library + artifact-ids: ${{ needs.build_libs.outputs.libs_macos }} path: dart/assets - name: Download libs uses: actions/download-artifact@v8 with: - name: windows-library + artifact-ids: ${{ needs.build_libs.outputs.libs_windows }} path: dart/assets - name: View downloaded artifacts diff --git a/dart/pubspec.lock b/dart/pubspec.lock index 933ed6e..7dfb4b1 100644 --- a/dart/pubspec.lock +++ b/dart/pubspec.lock @@ -458,7 +458,7 @@ packages: source: hosted version: "1.4.0" uuid: - dependency: transitive + dependency: "direct dev" description: name: uuid sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index e222315..dc5ecfe 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -18,6 +18,7 @@ dev_dependencies: convert: ^3.1.2 meta: ^1.16.0 path: ^1.9.1 + uuid: ^4.6.0 # See https://pub.dev/documentation/sqlite3/latest/topics/hook-topic.html hooks: diff --git a/dart/tool/generate_sbom.dart b/dart/tool/generate_sbom.dart new file mode 100644 index 0000000..b926350 --- /dev/null +++ b/dart/tool/generate_sbom.dart @@ -0,0 +1,159 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:uuid/uuid.dart'; + +/// Generates an SBOM for the core extension. +/// +/// Usage: `dart tool/generate_sbom.dart > bom.json` +void main() async { + final crates = await findDependencies(); + + const journeyApps = { + 'name': 'JourneyApps', + 'url': ['https://powersync.com', 'https://journeyapps.com/'], + }; + + final sbom = { + 'bomFormat': 'CycloneDX', + 'specVersion': '1.7', + 'serialNumber': 'urn:uuid:${const Uuid().v4()}', + if (Platform.environment['GITHUB_ACTIONS'] == 'true') + 'version': int.parse(Platform.environment['GITHUB_RUN_ID']!), + 'metadata': { + 'component': crates.first.describeAsBomComponent(), + 'lifecycles': [ + {'phase': 'build'} + ], + 'timestamp': DateTime.now().toIso8601String(), + 'manufacturer': journeyApps, + 'supplier': journeyApps, + }, + 'components': [ + for (final crate in crates.skip(1)) crate.describeAsBomComponent(), + ], + 'dependencies': [ + for (final crate in crates) + if (crate.dependencies.isNotEmpty) + { + 'ref': crate.bomRef, + 'dependsOn': [for (final dep in crate.dependencies) dep.bomRef] + } + ], + }; + + print(JsonEncoder.withIndent(' ' * 2).convert(sbom)); +} + +final class RustCrate { + final String name; + final String version; + final String licenseExpression; + final String repository; + + final List dependencies = []; + + String get bomRef => '$name-$version'; + + RustCrate({ + required this.name, + required this.version, + required this.licenseExpression, + required this.repository, + }); + + Map describeAsBomComponent() { + return { + 'version': version, + 'type': 'library', + 'bom-ref': bomRef, + 'name': name, + 'scope': 'required', + 'licenses': [ + {'expression': licenseExpression}, + ], + 'purl': 'pkg:cargo/${name}@${version}', + 'externalReferences': [ + {'url': repository, 'type': 'vcs'}, + if (name.contains('powersync')) + { + 'url': 'https://powersync.com/', + 'type': 'website', + } + ], + }; + } +} + +// Matches a single line of `cargo tree --prefix depth` output, e.g. +// `1serde v1.0.228;MIT OR Apache-2.0;https://github.com/serde-rs/serde` or +// `0powersync_core v0.5.2 (/path/to/crates/core);Apache-2.0;https://...`. +// Cargo dedupes subtrees it has already expanded elsewhere by appending +// ` (*)` and not descending further, which the trailing group strips. +final _treeLine = RegExp( + r'^(\d+)(\S+) v(\S+)(?: \([^)]*\))?;([^;]*);(.*?)(?: \(\*\))?$', +); + +Future> findDependencies() async { + final result = await Process.run('cargo', [ + 'tree', + '-p', + 'powersync_core', + '--edges', + 'normal,no-proc-macro', + '--prefix', + 'depth', + '--format', + '{p};{l};{r}', + ]); + if (result.exitCode != 0) { + throw 'cargo tree failed with exit code ${result.exitCode}:\n${result.stderr}'; + } + + final crates = {}; + // The chain of crates from the root to the current line, indexed by depth. + final stack = []; + + for (final line in const LineSplitter().convert(result.stdout as String)) { + if (line.trim().isEmpty) { + continue; + } + + final match = _treeLine.firstMatch(line); + if (match == null) { + throw 'Could not parse cargo tree line: $line'; + } + + final depth = int.parse(match[1]!); + final name = match[2]!; + final version = match[3]!; + final license = match[4]!; + final repository = match[5]!; + + // A crate is only fully expanded the first time it's encountered, so + // later (deduped) occurrences reuse the same instance and its already + // populated dependencies. + final crate = crates.putIfAbsent( + '$name@$version', + () => RustCrate( + name: name, + version: version, + licenseExpression: license, + repository: repository, + ), + ); + + if (depth < stack.length) { + stack.removeRange(depth, stack.length); + } + if (depth > 0) { + final parent = stack[depth - 1]; + if (!parent.dependencies.contains(crate)) { + parent.dependencies.add(crate); + } + } + stack.add(crate); + } + + return crates.values.toList(); +} From 0ebae040547c5977b6b7c405dd9b2e057507a4c8 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 11 Aug 2026 15:09:14 +0200 Subject: [PATCH 02/10] Fix release workflow --- .github/workflows/build.yml | 7 +++---- .github/workflows/release.yml | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 670f92a..caf9814 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,14 +11,13 @@ on: description: "Whether to sign the built Android library" default: false type: boolean + secrets: gpg-key: required: false description: "The GPG key to use when signing the publication" - type: string gpg-password: required: false description: "Password for the GPG key." - type: string outputs: libs_linux: description: "Artifact ID of the uploaded linux-library artifact" @@ -182,8 +181,8 @@ jobs: ls -lh build/outputs/aar find build/repository env: - INPUTS_GPG_KEY: ${{ inputs.gpg-key }} - INPUTS_GPG_PASSWORD: ${{ inputs.gpg-password }} + INPUTS_GPG_KEY: ${{ secrets.gpg-key }} + INPUTS_GPG_PASSWORD: ${{ secrets.gpg-password }} - name: Build library without signing shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93e08e4..37b83fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,6 +41,7 @@ jobs: with: attest: true sign-publication: true + secrets: gpg-key: ${{ secrets.GPG_PRIVATE_KEY }} gpg-password: ${{ secrets.GPG_PASSWORD }} From da3403d3efde24627c8ff21249e38d3386b2b34b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 11 Aug 2026 15:10:00 +0200 Subject: [PATCH 03/10] Test sbom generation --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6aa168e..e0c6966 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -126,6 +126,10 @@ jobs: run: | dart test -P ci + - name: Generate sbom + working-directory: dart + run: dart tool/generate_sbom.dart + build_stable_rust: runs-on: ubuntu-latest steps: From 57255c5394fb94f99b57fec2edb7ac55073efb9f Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 11 Aug 2026 15:24:51 +0200 Subject: [PATCH 04/10] Add attestation step --- .github/workflows/build.yml | 60 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 4 +++ .github/workflows/tests.yml | 6 ++++ 3 files changed, 70 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index caf9814..cb42651 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -307,3 +307,63 @@ jobs: compression-level: 0 # We're uploading a zip archive, no need to compress agan path: | powersync-sqlite-core.xcframework.zip + + attest: + name: Attest artifacts + if: ${{ inputs.attest }} + needs: + [ + libs_linux, + libs_macos, + libs_windows, + libs_android, + libs_wasm, + libs_xcframework, + ] + runs-on: ubuntu-latest + permissions: + id-token: write # Needed to mint the OIDC token used to sign the attestation. + attestations: write # Needed to publish the attestation. + artifact-metadata: write # Create artifact storage record. + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - uses: dart-lang/setup-dart@v1 + + - name: Install Rust Nightly + shell: bash + run: rustup install + + - name: Install Dart dependencies + working-directory: dart + run: dart pub get + + - name: Generate SBOM + working-directory: dart + run: dart tool/generate_sbom.dart > ../bom.json + + - name: Download build artifacts + uses: actions/download-artifact@v8 + with: + path: artifacts + merge-multiple: true + artifact-ids: >- + ${{ needs.libs_linux.outputs.artifact_id }}, + ${{ needs.libs_macos.outputs.artifact_id }}, + ${{ needs.libs_windows.outputs.artifact_id }}, + ${{ needs.libs_android.outputs.artifact_id }}, + ${{ needs.libs_android.outputs.static_artifact_id }}, + ${{ needs.libs_wasm.outputs.artifact_id }}, + ${{ needs.libs_xcframework.outputs.artifact_id }} + + - name: List downloaded artifacts + run: find artifacts + + - name: Attest artifacts + uses: actions/attest@v4 + if: false + with: + sbom-path: bom.json + subject-path: artifacts/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37b83fd..176c6c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,10 @@ jobs: build_libs: name: Build libraries uses: ./.github/workflows/build.yml + permissions: + id-token: write # Needed to mint the OIDC token used to sign the attestation. + attestations: write # Needed to publish the attestation. + artifact-metadata: write # Create artifact storage record. with: attest: true sign-publication: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e0c6966..3327fe1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,6 +27,12 @@ jobs: name: Build libraries if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) uses: ./.github/workflows/build.yml + permissions: + # Not used here since attest is not enabled for this workflow, but the + # reusable workflow's attest job requires the caller to grant these. + id-token: write # Unused + attestations: write # Unused + artifact-metadata: write # Unused rust_unit_tests: name: Rust unit tests on ${{ matrix.os }} From 2e9b6c2d865943911fbf3372b74b69f6004dfc90 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 11 Aug 2026 15:25:24 +0200 Subject: [PATCH 05/10] try attestation, part 1 --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3327fe1..eb0d1af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,6 +33,8 @@ jobs: id-token: write # Unused attestations: write # Unused artifact-metadata: write # Unused + with: + attest: true # Todo: remove rust_unit_tests: name: Rust unit tests on ${{ matrix.os }} From 02b032183cd426ec5aa298ff767eef2e34047128 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 11 Aug 2026 15:58:22 +0200 Subject: [PATCH 06/10] Declare SQLite dependency --- .github/workflows/build.yml | 2 +- crates/core/src/constants.rs | 3 +-- dart/tool/generate_sbom.dart | 29 ++++++++++++++++++++++++----- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb42651..a9ea7dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -366,4 +366,4 @@ jobs: if: false with: sbom-path: bom.json - subject-path: artifacts/ + subject-path: artifacts/* diff --git a/crates/core/src/constants.rs b/crates/core/src/constants.rs index e2a2fd0..1866214 100644 --- a/crates/core/src/constants.rs +++ b/crates/core/src/constants.rs @@ -5,8 +5,7 @@ pub const FULL_GIT_HASH: &'static str = env!("GIT_HASH"); // We need 3.44 or later to use an `ORDER BY` in an aggregate function invocation. // -// When raising the minimum version requirement, also change it in download_sqlite3.dart to ensure -// we're testing with the minimum version we claim to support. +// When raising the minimum version requirement, also change it in generate_sbom.dart. pub const MIN_SQLITE_VERSION_NUMBER: c_int = 3044000; pub const SUBTYPE_JSON: u32 = 'J' as u32; diff --git a/dart/tool/generate_sbom.dart b/dart/tool/generate_sbom.dart index b926350..bc69341 100644 --- a/dart/tool/generate_sbom.dart +++ b/dart/tool/generate_sbom.dart @@ -7,7 +7,7 @@ import 'package:uuid/uuid.dart'; /// /// Usage: `dart tool/generate_sbom.dart > bom.json` void main() async { - final crates = await findDependencies(); + final [coreExtension, ...dependencies] = await findDependencies(); const journeyApps = { 'name': 'JourneyApps', @@ -21,7 +21,7 @@ void main() async { if (Platform.environment['GITHUB_ACTIONS'] == 'true') 'version': int.parse(Platform.environment['GITHUB_RUN_ID']!), 'metadata': { - 'component': crates.first.describeAsBomComponent(), + 'component': coreExtension.describeAsBomComponent(), 'lifecycles': [ {'phase': 'build'} ], @@ -30,14 +30,33 @@ void main() async { 'supplier': journeyApps, }, 'components': [ - for (final crate in crates.skip(1)) crate.describeAsBomComponent(), + for (final crate in dependencies) crate.describeAsBomComponent(), + // Also declare SQLite as an external component required at runtime (since + // this is a SQLite extension). + { + 'isExternal': true, + 'versionRange': 'vers:semver/>=3.44.0|<4.0.0', + 'type': 'library', + 'name': 'SQLite', + 'purl': 'pkg:generic/sqlite', + 'bom-ref': 'external-sqlite', + 'licenses': [ + {'expression': 'blessing'}, + ], + 'externalReferences': [ + {'url': 'https://sqlite.org/', 'type': 'website'}, + ], + } ], 'dependencies': [ - for (final crate in crates) + for (final crate in [coreExtension, ...dependencies]) if (crate.dependencies.isNotEmpty) { 'ref': crate.bomRef, - 'dependsOn': [for (final dep in crate.dependencies) dep.bomRef] + 'dependsOn': [ + for (final dep in crate.dependencies) dep.bomRef, + if (crate == coreExtension) 'external-sqlite' + ] } ], }; From d201d4f8b8ca570d40beea6d9f827f8f79295304 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 11 Aug 2026 16:09:47 +0200 Subject: [PATCH 07/10] Don't skip attestation --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a9ea7dd..5fe7077 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -363,7 +363,6 @@ jobs: - name: Attest artifacts uses: actions/attest@v4 - if: false with: sbom-path: bom.json subject-path: artifacts/* From 483162a9b34113aa32e4cc7f2f75c948b13eb106 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 11 Aug 2026 16:28:37 +0200 Subject: [PATCH 08/10] Upload sbom --- .github/workflows/build.yml | 18 ++++++++++++++++++ .github/workflows/release.yml | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fe7077..d52d477 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,6 +40,9 @@ on: libs_xcframework: description: "Artifact ID of the uploaded xcframework artifact" value: ${{ jobs.libs_xcframework.outputs.artifact_id }} + sbom: + description: "Artifact ID of the uploaded sbom artifact" + value: ${{ jobs.attest.outputs.artifact_id }} permissions: {} @@ -321,6 +324,8 @@ jobs: libs_xcframework, ] runs-on: ubuntu-latest + outputs: + artifact_id: ${{ steps.upload_sbom.outputs.artifact-id }} permissions: id-token: write # Needed to mint the OIDC token used to sign the attestation. attestations: write # Needed to publish the attestation. @@ -344,6 +349,14 @@ jobs: working-directory: dart run: dart tool/generate_sbom.dart > ../bom.json + - name: Upload SBOM + uses: actions/upload-artifact@v4 + id: upload_sbom + with: + name: sbom + retention-days: 14 + path: bom.json + - name: Download build artifacts uses: actions/download-artifact@v8 with: @@ -362,6 +375,11 @@ jobs: run: find artifacts - name: Attest artifacts + uses: actions/attest@v4 + with: + subject-path: artifacts/* + + - name: Attest SBOM uses: actions/attest@v4 with: sbom-path: bom.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 176c6c5..95b3737 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,6 +161,10 @@ jobs: uses: actions/download-artifact@v8 with: artifact-ids: ${{ needs.build_libs.outputs.libs_android_static }} + - name: Download SBOM + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{ needs.build_libs.outputs.sbom }} - name: Create archive of static libs for Kotlin run: zip static_libs.zip *.lib *.a @@ -178,6 +182,7 @@ jobs: gh release upload "${TAG}" *.dylib gh release upload "${TAG}" *.so gh release upload "${TAG}" static_libs.zip + gh release upload "${TAG}" bom.json publish_wasm: name: Publish WASM builds From 4a44dd3cb75a9026f2dd08987033962da36b1ae4 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 11 Aug 2026 16:32:12 +0200 Subject: [PATCH 09/10] Disable attestation for tests --- .github/workflows/tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eb0d1af..3327fe1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,8 +33,6 @@ jobs: id-token: write # Unused attestations: write # Unused artifact-metadata: write # Unused - with: - attest: true # Todo: remove rust_unit_tests: name: Rust unit tests on ${{ matrix.os }} From ebe79e75627140d4132146c5fb309ee0db7eca1a Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 11 Aug 2026 17:29:00 +0200 Subject: [PATCH 10/10] AI feedback --- .github/workflows/build.yml | 7 ++----- dart/tool/generate_sbom.dart | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d52d477..4e45a80 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -146,7 +146,6 @@ jobs: libs_android: name: Building Android libraries - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) runs-on: ubuntu-latest outputs: artifact_id: ${{ steps.upload.outputs.artifact-id }} @@ -177,7 +176,7 @@ jobs: - name: Build signed library shell: bash - if: ${{ inputs.sign-publication == '1' }} + if: ${{ inputs.sign-publication }} run: | cd android ./gradlew build zipPublication -PgpgKey=${INPUTS_GPG_KEY} -PgpgPassword=${INPUTS_GPG_PASSWORD} @@ -189,7 +188,7 @@ jobs: - name: Build library without signing shell: bash - if: ${{ inputs.sign-publication == '0' }} + if: ${{ !inputs.sign-publication }} run: | cd android ./gradlew build zipPublication -PsignPublication=0 @@ -231,7 +230,6 @@ jobs: libs_wasm: name: Basic WASM build - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) runs-on: ubuntu-latest outputs: artifact_id: ${{ steps.upload.outputs.artifact-id }} @@ -257,7 +255,6 @@ jobs: libs_xcframework: name: Build XCFramework - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) runs-on: macos-latest outputs: artifact_id: ${{ steps.upload.outputs.artifact-id }} diff --git a/dart/tool/generate_sbom.dart b/dart/tool/generate_sbom.dart index bc69341..06eb751 100644 --- a/dart/tool/generate_sbom.dart +++ b/dart/tool/generate_sbom.dart @@ -7,7 +7,13 @@ import 'package:uuid/uuid.dart'; /// /// Usage: `dart tool/generate_sbom.dart > bom.json` void main() async { + // The first library is the core extension itself (the powersync_core crate). + // The crate we actually compile as binaries is powersync_static or + // powersync_loadable, but these contain no additional dependencies. final [coreExtension, ...dependencies] = await findDependencies(); + if (coreExtension.name != 'powersync_core') { + throw 'Unexpected root: ${coreExtension.name}'; + } const journeyApps = { 'name': 'JourneyApps', @@ -18,8 +24,7 @@ void main() async { 'bomFormat': 'CycloneDX', 'specVersion': '1.7', 'serialNumber': 'urn:uuid:${const Uuid().v4()}', - if (Platform.environment['GITHUB_ACTIONS'] == 'true') - 'version': int.parse(Platform.environment['GITHUB_RUN_ID']!), + 'version': 1, 'metadata': { 'component': coreExtension.describeAsBomComponent(), 'lifecycles': [ @@ -72,7 +77,7 @@ final class RustCrate { final List dependencies = []; - String get bomRef => '$name-$version'; + String get bomRef => '$name@$version'; RustCrate({ required this.name, @@ -149,6 +154,10 @@ Future> findDependencies() async { final license = match[4]!; final repository = match[5]!; + if (license.isEmpty) { + throw 'Crate $name@$version does not include a license expression.'; + } + // A crate is only fully expanded the first time it's encountered, so // later (deduped) occurrences reuse the same instance and its already // populated dependencies.