diff --git a/.github/workflows/oranda.yml b/.github/workflows/oranda.yml index d05f58e..7d59b21 100644 --- a/.github/workflows/oranda.yml +++ b/.github/workflows/oranda.yml @@ -79,7 +79,7 @@ jobs: cp public/index.html /tmp/public - name: Check HTML for broken internal links - uses: untitaker/hyperlink@0.1.32 + uses: untitaker/hyperlink@0.1.44 with: args: /tmp/public diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf3ce65..20bf065 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,21 @@ -# Copyright 2022-2023, axodotdev +# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ +# +# Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: # # * checks for a Git Tag that looks like a release -# * builds artifacts with cargo-dist (archives, installers, hashes) +# * builds artifacts with dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip -# * on success, uploads the artifacts to a Github Release +# * on success, uploads the artifacts to a GitHub Release # -# Note that the Github Release will be created with a generated +# Note that the GitHub Release will be created with a generated # title/body based on your changelogs. name: Release - permissions: - contents: write + "contents": "write" # This task will run whenever you push a git tag that looks like a version # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. @@ -23,15 +24,15 @@ permissions: # must be a Cargo-style SemVer Version (must have at least major.minor.patch). # # If PACKAGE_NAME is specified, then the announcement will be for that -# package (erroring out if it doesn't have the given version or isn't cargo-dist-able). +# package (erroring out if it doesn't have the given version or isn't dist-able). # # If PACKAGE_NAME isn't specified, then the announcement will be for all -# (cargo-dist-able) packages in the workspace with that version (this mode is +# (dist-able) packages in the workspace with that version (this mode is # intended for workspaces with only one dist-able package, or with all dist-able # packages versioned/released in lockstep). # # If you push multiple tags at once, separate instances of this workflow will -# spin up, creating an independent announcement for each one. However Github +# spin up, creating an independent announcement for each one. However, GitHub # will hard limit this to 3 tags per commit, as it will assume more tags is a # mistake. # @@ -43,9 +44,9 @@ on: - '**[0-9]+.[0-9]+.[0-9]+*' jobs: - # Run 'cargo dist plan' (or host) to determine what tasks we need to do + # Run 'dist plan' (or host) to determine what tasks we need to do plan: - runs-on: ubuntu-latest + runs-on: "ubuntu-20.04" outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} @@ -57,11 +58,16 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install cargo-dist + - name: Install dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.11.1/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.26.1/cargo-dist-installer.sh | sh" + - name: Cache dist + uses: actions/upload-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/dist # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. @@ -69,8 +75,8 @@ jobs: # but also really annoying to build CI around when it needs secrets to work right.) - id: plan run: | - cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json - echo "cargo dist ran successfully" + dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "dist ran successfully" cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" @@ -88,28 +94,38 @@ jobs: if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} strategy: fail-fast: false - # Target platforms/runners are computed by cargo-dist in create-release. + # Target platforms/runners are computed by dist in create-release. # Each member of the matrix has the following arguments: # # - runner: the github runner - # - dist-args: cli flags to pass to cargo dist - # - install-dist: expression to run to install cargo-dist on the runner + # - dist-args: cli flags to pass to dist + # - install-dist: expression to run to install dist on the runner # # Typically there will be: # - 1 "global" task that builds universal installers # - N "local" tasks that build each platform's binaries and platform-specific installers matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} runs-on: ${{ matrix.runner }} + container: ${{ matrix.container && matrix.container.image || null }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true - uses: actions/checkout@v4 with: submodules: recursive - - uses: swatinem/rust-cache@v2 - - name: Install cargo-dist - run: ${{ matrix.install_dist }} + - name: Install Rust non-interactively if not already installed + if: ${{ matrix.container }} + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + - name: Install dist + run: ${{ matrix.install_dist.run }} # Get the dist-manifest - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -123,8 +139,8 @@ jobs: - name: Build artifacts run: | # Actually do builds and make zips and whatnot - cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json - echo "cargo dist ran successfully" + dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "dist ran successfully" - id: cargo-dist name: Post-build # We force bash here just because github makes it really hard to get values up @@ -134,7 +150,7 @@ jobs: run: | # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" + dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -159,9 +175,12 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install cargo-dist - shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.11.1/cargo-dist-installer.sh | sh" + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -172,12 +191,12 @@ jobs: - id: cargo-dist shell: bash run: | - cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json - echo "cargo dist ran successfully" + dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "dist ran successfully" # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -205,8 +224,12 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install cargo-dist - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.11.1/cargo-dist-installer.sh | sh" + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 @@ -214,11 +237,10 @@ jobs: pattern: artifacts-* path: target/distrib/ merge-multiple: true - # This is a harmless no-op for Github Releases, hosting for that happens in "announce" - id: host shell: bash run: | - cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" @@ -228,8 +250,29 @@ jobs: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json + # Create a GitHub Release while uploading all files to it + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create GitHub Release + env: + PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" + ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" + ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" + RELEASE_COMMIT: "${{ github.sha }}" + run: | + # Write and read notes from a file to avoid quoting breaking things + echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt + + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* - # Create a Github Release while uploading all files to it announce: needs: - plan @@ -245,21 +288,3 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - name: "Download Github Artifacts" - uses: actions/download-artifact@v4 - with: - pattern: artifacts-* - path: artifacts - merge-multiple: true - - name: Cleanup - run: | - # Remove the granular manifests - rm -f artifacts/*-dist-manifest.json - - name: Create Github Release - uses: ncipollo/release-action@v1 - with: - tag: ${{ needs.plan.outputs.tag }} - name: ${{ fromJson(needs.host.outputs.val).announcement_title }} - body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }} - prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }} - artifacts: "artifacts/*" diff --git a/Cargo.lock b/Cargo.lock index 374c2c5..58c06c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,66 +1,61 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "anstream" -version = "0.6.11" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys", ] -[[package]] -name = "anyhow" -version = "1.0.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" - [[package]] name = "clap" -version = "4.5.0" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -68,9 +63,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.0" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -80,9 +75,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -92,89 +87,115 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "json2env" -version = "0.2.0" +version = "0.3.1" dependencies = [ - "anyhow", "clap", "serde_json", ] [[package]] -name = "proc-macro2" -version = "1.0.78" +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.28" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" -version = "1.0.97" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.18" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -183,144 +204,85 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.0", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnu" -version = "0.52.0" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 0927352..0ac455a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json2env" -version = "0.2.0" +version = "0.3.1" edition = "2021" authors = ["Marcello Lamonaca "] description = "JSON to Env Var converter" @@ -10,7 +10,6 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.71" clap = { version = "4.5.0", features = ["derive", "color"] } serde_json = "1.0.97" @@ -18,16 +17,3 @@ serde_json = "1.0.97" [profile.dist] inherits = "release" lto = "thin" - -# Config for 'cargo dist' -[workspace.metadata.dist] -# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.11.1" -# CI backends to support -ci = ["github"] -# The installers to generate for each app -installers = ["shell", "powershell"] -# Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] -# Publish jobs to run in CI -pr-run-mode = "skip" diff --git a/README.md b/README.md index c278f5a..d9fbf0d 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,20 @@ -# json2env - -Convert valid JSON to environment variables or an `.env`-line file. - -## Usage - -```sh -JSON to Env Var converter - -Usage: json2env.exe [OPTIONS] - -Options: - -i, --input Input file, defaults to STDIN if not specified - -o, --output Output file, defaults to STDOUT if not specified - -s, --separator Separator for nested keys - -h, --help Print help - -V, --version Print version -``` - -## Installation - -You can either install the tool with `cargo`: - -```sh -cargo install --path -``` - -or build the executable with (output in `target/release`): - -```sh -cargo build --release -``` +# json2env + +Convert valid JSON to environment variables or an `.env`-line file. + +## Usage + +```sh +JSON to Env Var converter + +Usage: json2env.exe [OPTIONS] + +Options: + -i, --input Input file, defaults to STDIN if not specified + -o, --output Output file, defaults to STDOUT if not specified + -s, --key-separator Separator for nested keys [default: __] + -S, --array-separator Separator for array elements [default: ,] + -e, --enumerate-array Separate array elements in multiple environment variables + -h, --help Print help + -V, --version Print version +``` diff --git a/dist-workspace.toml b/dist-workspace.toml new file mode 100644 index 0000000..70ddcaa --- /dev/null +++ b/dist-workspace.toml @@ -0,0 +1,19 @@ +[workspace] +members = ["cargo:."] + +# Config for 'dist' +[dist] +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.26.1" +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = ["shell", "powershell"] +# Target platforms to build apps for (Rust target-triple syntax) +targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-pc-windows-msvc", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] +# Which actions to run on pull requests +pr-run-mode = "skip" +# Path that installers should place binaries in +install-path = "CARGO_HOME" +# Whether to install an updater program +install-updater = false diff --git a/oranda.json b/oranda.json index 913a29c..8842157 100644 --- a/oranda.json +++ b/oranda.json @@ -1,5 +1,15 @@ -{ - "build": { - "path_prefix": "json-to-env" - } +{ + "$schema": "https://github.com/axodotdev/oranda/releases/latest/download/oranda-config-schema.json", + "build": { + "path_prefix": "json-to-env" + }, + "styles": { + "theme": "hacker" + }, + "components": { + "changelog": true, + "artifacts": { + "cargo_dist": true + } + } } \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..dc676d3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,242 @@ +use std::fmt::Display; + +use serde_json::Value; + +#[derive(Debug, Clone)] +pub struct ParseOptions { + key_separator: String, + array_separator: String, + enumerate_array: bool, +} + +impl ParseOptions { + pub fn new(key_separator: String, array_separator: String, enumerate_array: bool) -> Self { + Self { + key_separator, + array_separator, + enumerate_array, + } + } +} + +#[derive(Debug, Clone)] +pub struct JsonParser { + options: ParseOptions, +} + +impl JsonParser { + pub fn new(options: ParseOptions) -> Self { + Self { options } + } + + pub fn parse(&mut self, json: &Value) -> Vec { + Self::parse_value("", json, &self.options) + } + + fn parse_value(key: &str, value: &Value, options: &ParseOptions) -> Vec { + match value { + Value::Array(array) => { + let has_complex_values = array + .iter() + .any(|value| value.is_object() || value.is_array()); + + // complex (nested) values cannot be part of an array enumeration, skip just this array + if options.enumerate_array || has_complex_values { + let mut values = Vec::with_capacity(array.len()); + + for (index, item) in array.iter().enumerate() { + let key = Self::build_key(key, &index.to_string(), &options.key_separator); + values.push(Self::parse_value(&key, item, options)); + } + + values.into_iter().flatten().collect() + } else { + let value = array + .iter() + .map(|value| value.to_string().replace(['\\', '"'], "")) + .collect::>() + .join(&options.array_separator); + + let value = serde_json::Value::String(value); + Self::parse_value(key, &value, options) + } + } + Value::Object(object) => { + let mut values = Vec::with_capacity(object.len()); + + for (name, value) in object.iter() { + let key = Self::build_key(key, name, &options.key_separator); + values.push(Self::parse_value(&key, value, options)); + } + + values.into_iter().flatten().collect() + } + _ => vec![EnvVar(key.trim().to_owned(), value.clone())], + } + } + + fn build_key(prefix: &str, key: &str, separator: &str) -> String { + match prefix.is_empty() { + true => key.to_string(), + false => format!("{prefix}{separator}{key}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnvVar(String, Value); + +impl Display for EnvVar { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.1 { + Value::Null => write!(fmt, "{key}=null", key = self.0), + Value::Bool(bool) => write!(fmt, "{key}={bool}", key = self.0), + Value::Number(ref number) => write!(fmt, "{key}={number}", key = self.0), + Value::String(ref string) => write!( + fmt, + r#"{key}="{value}""#, + key = self.0, + value = string.replace('"', r#"\""#) + ), + _ => write!(fmt, ""), + } + } +} + +#[cfg(test)] +mod tests { + use serde_json::{json, Value}; + + use crate::{EnvVar, JsonParser, ParseOptions}; + + const KEY: &str = r#""key""#; + + #[test] + fn build_key_should_leave_key_unchanged_when_prefix_is_empty() { + // ARRANGE + let separator = ""; + let input = KEY.to_owned(); + let expected = KEY; + + // ACT + let result = JsonParser::build_key("", &input, separator); + + // ASSERT + assert_eq!(result, expected); + } + + #[test] + fn build_key_should_leave_prepend_prefix_with_separator() { + // ARRANGE + let separator = "_"; + let input = KEY.to_owned(); + let expected = format!("prefix{separator}{KEY}"); + + // ACT + let actual = JsonParser::build_key("prefix", &input, separator); + + // ASSERT + assert_eq!(actual, expected); + } + + #[test] + fn bool_env_var_should_be_formatted_correctly() { + // ARRANGE + let input = EnvVar(KEY.to_owned(), json!(true)); + + // ACT + let result = input.to_string(); + + // ASSERT + assert_eq!(result, r#""key"=true"#) + } + + #[test] + fn numeric_env_var_should_be_formatted_correctly() { + // ARRANGE + let input = EnvVar(KEY.to_owned(), json!(1.0)); + + // ACT + let result = input.to_string(); + + // ASSERT + assert_eq!(result, r#""key"=1.0"#) + } + + #[test] + fn string_env_var_should_be_formatted_correctly() { + // ARRANGE + let input = EnvVar(KEY.to_owned(), json!("hello")); + + // ACT + let result = input.to_string(); + + // ASSERT + assert_eq!(result, r#""key"="hello""#) + } + + #[test] + fn array_env_var_should_be_formatted_correctly() { + // ARRANGE + let input = EnvVar(KEY.to_owned(), json!([1, 2])); + + // ACT + let result = input.to_string(); + + // ASSERT + assert_eq!(result, "") + } + + #[test] + fn object_env_var_should_be_formatted_correctly() { + // ARRANGE + let input = EnvVar(KEY.to_owned(), json!({ "key": "value" })); + + // ACT + let result = input.to_string(); + + // ASSERT + assert_eq!(result, "") + } + + #[test] + fn parse_array_not_enumerated() { + // ARRANGE + let json = json!({ "array": [1, 2, 3] }); + let options = ParseOptions::new("__".to_string(), ",".to_string(), false); + let mut parser = JsonParser::new(options); + + // ACT + let environ = parser.parse(&json); + + // ASSERT + assert_eq!( + *environ, + vec![EnvVar( + "array".to_string(), + Value::String("1,2,3".to_string()) + )] + ) + } + + #[test] + fn parse_array_enumerated() { + // ARRANGE + let json = json!({ "array": [1, 2, 3] }); + let options = ParseOptions::new("__".to_string(), ",".to_string(), true); + let mut parser = JsonParser::new(options); + + // ACT + let environ = parser.parse(&json); + + // ASSERT + assert_eq!( + *environ, + vec![ + EnvVar("array__0".to_string(), Value::Number(1.into())), + EnvVar("array__1".to_string(), Value::Number(2.into())), + EnvVar("array__2".to_string(), Value::Number(3.into())) + ] + ) + } +} diff --git a/src/main.rs b/src/main.rs index d2975d4..e0cbf77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,21 @@ use std::{ - fmt::Display, + error::Error, fs::File, io::{BufRead, BufReader, BufWriter, Read, Write}, }; -use anyhow::{Context, Result}; use clap::Parser; +use json2env::{JsonParser, ParseOptions}; use serde_json::Value; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { let args = Args::parse(); let mut reader: Box = match args.input { None => Box::new(std::io::stdin().lock()), Some(ref filename) => { let file = File::open(filename) - .with_context(|| format!("Could not open file `{filename}`"))?; + .inspect_err(|_| eprintln!("Error: Could not open file `{filename}`"))?; Box::new(BufReader::new(file)) } @@ -26,15 +26,21 @@ fn main() -> Result<()> { let input = args.input.unwrap_or("STDIN".to_string()); reader .read_to_string(&mut buffer) - .with_context(|| format!("Could not read `{input}`"))?; + .inspect_err(|_| eprintln!("Error: Could not read `{input}`"))?; let json: Value = serde_json::from_str(&buffer) - .with_context(|| format!("`{input}` does not contain valid JSON"))?; + .inspect_err(|_| eprintln!("Error: `{input}` does not contain valid JSON"))?; - let mut vars: Vec = vec![]; - JsonParser::parse(&mut vars, "", &json, &args.separator); + let options = ParseOptions::new( + args.key_separator, + args.array_separator, + args.enumerate_array, + ); - let environ = vars + let mut parser = JsonParser::new(options); + let keys = parser.parse(&json); + + let environ = keys .iter() .map(ToString::to_string) .collect::>() @@ -44,7 +50,7 @@ fn main() -> Result<()> { None => Box::new(std::io::stdout().lock()), Some(ref filename) => { let file = File::create(filename) - .with_context(|| format!("Could not open file `{filename}`"))?; + .inspect_err(|_| eprintln!("Error: Could not open file `{filename}`"))?; Box::new(BufWriter::new(file)) } @@ -53,7 +59,7 @@ fn main() -> Result<()> { let output = args.output.unwrap_or("STDOUT".to_string()); writer .write_all(environ.as_bytes()) - .with_context(|| format!("Could not write to `{output}`"))?; + .inspect_err(|_| eprintln!("Error: Could not write to `{output}`"))?; Ok(()) } @@ -70,151 +76,14 @@ struct Args { output: Option, /// Separator for nested keys - #[arg(short, long, value_name = "STRING", default_value = "__")] - separator: String, -} - -struct JsonParser; - -impl JsonParser { - fn parse(lines: &mut Vec, key: &str, value: &Value, separator: &str) { - match value { - Value::Array(array) => { - for (index, item) in array.iter().enumerate() { - let key = Self::build_key(key, index.to_string().as_str(), separator); - Self::parse(lines, &key, item, separator) - } - } - Value::Object(object) => { - for (name, value) in object { - let key = Self::build_key(key, name.as_str(), separator); - Self::parse(lines, &key, value, separator) - } - } - _ => lines.push(EnvVar(key.trim().to_owned(), value.clone())), - } - } - - fn build_key(prefix: &str, key: &str, separator: &str) -> String { - match prefix.is_empty() { - true => key.to_string(), - false => format!("{prefix}{separator}{key}"), - } - } -} - -struct EnvVar(String, Value); - -impl Display for EnvVar { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.1 { - Value::Null => write!(fmt, "{key}=null", key = self.0), - Value::Bool(bool) => write!(fmt, "{key}={bool}", key = self.0), - Value::Number(ref number) => write!(fmt, "{key}={number}", key = self.0), - Value::String(ref string) => write!( - fmt, - r#"{key}="{value}""#, - key = self.0, - value = string.replace('"', r#"\""#) - ), - _ => write!(fmt, ""), - } - } -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use crate::{EnvVar, JsonParser}; - - const KEY: &str = r#""key""#; - - #[test] - fn build_key_should_leave_key_unchanged_when_prefix_is_empty() { - // ARRANGE - let separator = ""; - let input = KEY.to_owned(); - let expected = KEY; - - // ACT - let result = JsonParser::build_key("", &input, separator); - - // ASSERT - assert_eq!(result, expected); - } - - #[test] - fn build_key_should_leave_prepend_prefix_with_separator() { - // ARRANGE - let separator = "_"; - let input = KEY.to_owned(); - let expected = format!("prefix{separator}{KEY}"); - - // ACT - let actual = JsonParser::build_key("prefix", &input, separator); - - // ASSERT - assert_eq!(actual, expected); - } - - #[test] - fn bool_env_var_should_be_formatted_correctly() { - // ARRANGE - let input = EnvVar(KEY.to_owned(), json!(true)); - - // ACT - let result = input.to_string(); - - // ASSERT - assert_eq!(result, r#""key"=true"#) - } - - #[test] - fn numeric_env_var_should_be_formatted_correctly() { - // ARRANGE - let input = EnvVar(KEY.to_owned(), json!(1.0)); - - // ACT - let result = input.to_string(); - - // ASSERT - assert_eq!(result, r#""key"=1.0"#) - } - - #[test] - fn string_env_var_should_be_formatted_correctly() { - // ARRANGE - let input = EnvVar(KEY.to_owned(), json!("hello")); - - // ACT - let result = input.to_string(); - - // ASSERT - assert_eq!(result, r#""key"="hello""#) - } - - #[test] - fn array_env_var_should_be_formatted_correctly() { - // ARRANGE - let input = EnvVar(KEY.to_owned(), json!([1, 2])); - - // ACT - let result = input.to_string(); - - // ASSERT - assert_eq!(result, "") - } - - #[test] - fn object_env_var_should_be_formatted_correctly() { - // ARRANGE - let input = EnvVar(KEY.to_owned(), json!({ "key": "value" })); - - // ACT - let result = input.to_string(); - - // ASSERT - assert_eq!(result, "") - } + #[arg(short = 's', long, value_name = "STRING", default_value = "__")] + key_separator: String, + + /// Separator for array elements + #[arg(short = 'S', long, value_name = "STRING", default_value = ",")] + array_separator: String, + + /// Separate array elements in multiple environment variables + #[arg(short, long)] + enumerate_array: bool, }