diff --git a/.github/actions/build-binaries/macos/action.yaml b/.github/actions/build-binaries/macos/action.yaml index 53e38c67f..360ba8358 100644 --- a/.github/actions/build-binaries/macos/action.yaml +++ b/.github/actions/build-binaries/macos/action.yaml @@ -9,6 +9,21 @@ inputs: required: true version: description: "The version to use for this artifact" + apple_team_id: + description: "The Apple Team ID" + required: true + apple_bundle_id: + description: "The bundle ID to be used for packaging and notarisation" + required: true + apple_cert_id: + description: "The Apple Developer ID certificate ID" + required: true + apple_notary_user: + description: "The Apple user to notarise the package" + require: true + apple_notary_password: + description: "The Apple password to notarise the package" + require: true runs: using: "composite" @@ -16,17 +31,44 @@ runs: - name: Build binary shell: bash run: | - poetry run poe package_unix + export APPLE_CERT_ID="${{ inputs.apple_cert_id }}" + export APPLE_BUNDLE_ID="${{ inputs.apple_bundle_id }}" + poetry run poe package_mac + env: + APPLE_CERT_ID: ${{ inputs.apple_cert_id }} + APPLE_BUNDLE_ID: ${{ inputs.apple_bundle_id }} + + - name: Add metadata to binary + shell: bash + run: | + echo brew > ${{ github.workspace }}/dist/algokit/_internal/algokit/resources/distribution-method + + # Workaround an issue with PyInstaller where Python.framework was incorrectly signed during the build + - name: Codesign python.framework + shell: bash + run: | + codesign --force --sign "${{ inputs.apple_cert_id }}" --timestamp "${{ github.workspace }}/dist/algokit/_internal/Python.framework" + + - name: Notarize + uses: lando/notarize-action@v2 + with: + appstore-connect-team-id: ${{ inputs.apple_team_id }} + appstore-connect-username: ${{ inputs.apple_notary_user }} + appstore-connect-password: ${{ inputs.apple_notary_password }} + primary-bundle-id: ${{ inputs.apple_bundle_id }} + product-path: "${{ github.workspace }}/dist/algokit" + tool: notarytool + verbose: true - name: Package binary artifact shell: bash run: | cd dist/algokit/ - tar -zcf ${{ inputs.artifacts_dir }}/${{ inputs.package_name }}.tar.gz * + tar -zcf ${{ inputs.artifacts_dir }}/${{ inputs.package_name }}-brew.tar.gz * cd ../.. - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: ${{ inputs.package_name }} - path: ${{ inputs.artifacts_dir }}/${{ inputs.package_name }}.tar.gz + path: ${{ inputs.artifacts_dir }}/${{ inputs.package_name }}-brew.tar.gz diff --git a/.github/actions/install-apple-dev-id-cert/action.yaml b/.github/actions/install-apple-dev-id-cert/action.yaml new file mode 100644 index 000000000..98691bcc0 --- /dev/null +++ b/.github/actions/install-apple-dev-id-cert/action.yaml @@ -0,0 +1,40 @@ +name: "Install Apple Developer ID certificate" +description: "Install Apple Developer ID certificate to macos-build keychain" +inputs: + cert_data: + description: "Base64 string represents the Apple developer ID certificate" + required: true + cert_password: + description: "The password to unlock the Apple developer ID certificate" + required: true + +runs: + using: "composite" + steps: + - name: Install cert + shell: bash + env: + APPLE_CERT_DATA: ${{ inputs.cert_data }} + APPLE_CERT_PASSWORD: ${{ inputs.cert_password }} + run: | + # Export certs + echo "$APPLE_CERT_DATA" | base64 --decode > /tmp/certs.p12 + + # Create keychain + security create-keychain -p actions macos-build.keychain + security default-keychain -s macos-build.keychain + security unlock-keychain -p actions macos-build.keychain + security set-keychain-settings -t 3600 -u macos-build.keychain + echo "Keychain created" + + # Import certs to keychain + security import /tmp/certs.p12 -k ~/Library/Keychains/macos-build.keychain -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign + echo "Cert imported" + + # Key signing + security set-key-partition-list -S apple-tool:,apple: -s -k actions macos-build.keychain + echo "Key signed" + + # Delete temp file + rm /tmp/certs.p12 + echo "Done" diff --git a/.github/workflows/build-binaries.yaml b/.github/workflows/build-binaries.yaml index 7530fde32..cd711b477 100644 --- a/.github/workflows/build-binaries.yaml +++ b/.github/workflows/build-binaries.yaml @@ -71,6 +71,13 @@ jobs: version: ${{ inputs.release_version }} artifacts_dir: ${{ env.ARTIFACTS_DIR }} + - name: Install Apple Developer Id Cert + if: runner.os == 'macOS' + uses: ./.github/actions/install-apple-dev-id-cert + with: + cert_data: ${{ secrets.APPLE_CERT_DATA }} + cert_password: ${{ secrets.APPLE_CERT_PASSWORD }} + - name: Build macOS binary if: ${{ runner.os == 'macOS' }} uses: ./.github/actions/build-binaries/macos @@ -78,6 +85,11 @@ jobs: package_name: ${{ env.PACKAGE_NAME }} version: ${{ inputs.release_version }} artifacts_dir: ${{ env.ARTIFACTS_DIR }} + apple_team_id: ${{ secrets.APPLE_TEAM_ID }} + apple_bundle_id: ${{ inputs.production_release == 'true' && vars.APPLE_BUNDLE_ID || format('beta.{0}', vars.APPLE_BUNDLE_ID) }} + apple_cert_id: ${{ secrets.APPLE_CERT_ID }} + apple_notary_user: ${{ secrets.APPLE_NOTARY_USER }} + apple_notary_password: ${{ secrets.APPLE_NOTARY_PASSWORD }} - name: Add binary to path run: | diff --git a/.github/workflows/publish-release-packages.yaml b/.github/workflows/publish-release-packages.yaml index ab2b0e14d..5c4d0cf04 100644 --- a/.github/workflows/publish-release-packages.yaml +++ b/.github/workflows/publish-release-packages.yaml @@ -3,7 +3,7 @@ name: Publish packages to public repositories on: workflow_call: inputs: - artifactName: + wheelArtifactName: required: true type: string description: "The github artifact holding the wheel file which will be published" @@ -67,24 +67,40 @@ jobs: uses: actions/checkout@v4 # Download either via release or provided artifact - - name: Download release + - name: Download wheel from release if: ${{ github.event_name == 'workflow_dispatch' }} run: gh release download v${{ inputs.release_version }} --pattern "*.whl" --dir dist env: GH_TOKEN: ${{ github.token }} - - name: Download artifact + - name: Download wheel from artifact if: ${{ github.event_name == 'workflow_call' }} uses: actions/download-artifact@v4 with: - name: ${{ inputs.artifactName }} + name: ${{ inputs.wheelArtifactName }} + path: dist + + - name: Download macOS binary from release + if: ${{ github.event_name == 'workflow_dispatch' }} + run: gh release download ${{ inputs.release }} --pattern "*-brew.tar.gz" --dir dist + env: + GH_TOKEN: ${{ github.token }} + + - name: Download macOS binary from artifact + uses: actions/download-artifact@v4 + if: ${{ github.event_name == 'workflow_call' }} + with: + name: ${{ inputs.binaryArtifactName }} path: dist - name: Set Git user as GitHub actions run: git config --global user.email "actions@github.com" && git config --global user.name "github-actions" + - name: ls dist folder + run: ls -la dist + - name: Update homebrew cask - run: scripts/update-brew-cask.sh "dist/algokit*-py3-none-any.whl" "algorandfoundation/homebrew-tap" + run: scripts/update-brew-cask.sh "dist/algokit*-py3-none-any.whl" "dist/algokit*-macos_arm64-brew.tar.gz" "dist/algokit*-macos_x64-brew.tar.gz" "algorandfoundation/homebrew-tap" env: TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} diff --git a/entitlements.xml b/entitlements.xml new file mode 100644 index 000000000..5d390840c --- /dev/null +++ b/entitlements.xml @@ -0,0 +1,10 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/pyproject.toml b/pyproject.toml index d52a89ffe..ebd482bdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,8 @@ docs_title = {shell = "(echo \"# AlgoKit CLI Reference Documentation\\n\\n\"; ca docs = ["docs_generate", "docs_toc", "docs_title"] package_unix = "pyinstaller --clean --onedir --hidden-import jinja2_ansible_filters --hidden-import multiformats_config --copy-metadata algokit --name algokit --noconfirm src/algokit/__main__.py --add-data './misc/multiformats_config:multiformats_config/' --add-data './src/algokit/resources:algokit/resources/'" package_windows = { cmd = "scripts/package_windows.bat" } +package_mac = "pyinstaller --clean --onedir --hidden-import jinja2_ansible_filters --hidden-import multiformats_config --copy-metadata algokit --name algokit --noconfirm src/algokit/__main__.py --add-data './misc/multiformats_config/multibase-table.json:multiformats_config/' --add-data './misc/multiformats_config/multicodec-table.json:multiformats_config/' --add-data './src/algokit/resources:algokit/resources/' --osx-bundle-identifier \"$APPLE_BUNDLE_ID\" --codesign-identity \"$APPLE_CERT_ID\" --osx-entitlements-file './entitlements.xml'" + [tool.ruff] line-length = 120 lint.select = [ diff --git a/scripts/update-brew-cask.sh b/scripts/update-brew-cask.sh index 3584fbd67..0decdbe43 100755 --- a/scripts/update-brew-cask.sh +++ b/scripts/update-brew-cask.sh @@ -3,15 +3,20 @@ #script arguments wheel_files=( $1 ) wheel_file=${wheel_files[0]} -homebrew_tap_repo=$2 +arm_artifacts=( $2 ) +arm_artifact=${arm_artifacts[0]} +intel_artifacts=( $3 ) +intel_artifact=${intel_artifacts[0]} +homebrew_tap_repo=$4 #globals command=algokit #error codes MISSING_WHEEL=1 -CASK_GENERATION_FAILED=2 -PR_CREATION_FAILED=3 +MISSING_EXECUTABLE=2 +CASK_GENERATION_FAILED=3 +PR_CREATION_FAILED=4 if [[ ! -f $wheel_file ]]; then >&2 echo "$wheel_file not found. 🚫" @@ -20,6 +25,21 @@ else echo "Found $wheel_file 🎉" fi +if [[ ! -f $arm_artifact ]]; then + >&2 echo "$arm_artifact not found. 🚫" + exit $MISSING_EXECUTABLE +else + echo "Found $arm_artifact 🎉" +fi + +if [[ ! -f $intel_artifact ]]; then + >&2 echo "$intel_artifact not found. 🚫" + exit $MISSING_EXECUTABLE +else + echo "Found $intel_artifact 🎉" +fi + + get_metadata() { local field=$1 grep "^$field:" $metadata | cut -f 2 -d : | xargs @@ -29,10 +49,10 @@ create_cask() { repo="https://github.com/${GITHUB_REPOSITORY}" homepage="$repo" - wheel=`basename $wheel_file` - echo "Creating brew cask from $wheel_file" + echo "Creating brew cask" - #determine package_name, version and release tag from .whl + # determine package_name, version and release tag from .whl + wheel=`basename $wheel_file` package_name=`echo $wheel | cut -d- -f1` version=None @@ -50,78 +70,66 @@ create_cask() { echo Version: $version echo Release Tag: $release_tag - url="$repo/releases/download/$release_tag/$wheel" - #get other metadata from wheel + # get other metadata from wheel unzip -o $wheel_file -d . >/dev/null 2>&1 metadata=`echo $wheel | cut -f 1,2 -d "-"`.dist-info/METADATA desc=`get_metadata Summary` license=`get_metadata License` - echo "Calculating sha256 of $url..." - sha256=`curl -s -L $url | sha256sum | cut -f 1 -d ' '` + arm_binary_url="$repo/releases/download/$release_tag/$(basename $arm_artifact)" + echo "Calculating sha256 of $arm_binary_url..." + arm_sha256=`curl -s -L $arm_binary_url | sha256sum | cut -f 1 -d ' '` - ruby=${command}.rb - - echo "Outputting $ruby..." + intel_binary_url="$repo/releases/download/$release_tag/$(basename $intel_artifact)" + echo "Calculating sha256 of $intel_binary_url..." + intel_sha256=`curl -s -L $intel_binary_url | sha256sum | cut -f 1 -d ' '` -cat << EOF > $ruby -# typed: false -# frozen_string_literal: true + cask_file=${command}.rb + + echo "Outputting $cask_file..." -cask "$command" do +cat << EOF > $cask_file +cask "$package_name" do + arch arm: "arm64", intel: "x64" version "$version" - sha256 "$sha256" + sha256 arm: "$arm_sha256", intel: "$intel_sha256" - url "$repo/releases/download/v#{version}/algokit-#{version}-py3-none-any.whl" - name "$command" + url "$repo/releases/download/v#{version}/algokit-#{version}-macos_#{arch}.tar.gz" + name "$package_name" desc "$desc" homepage "$homepage" - depends_on formula: "pipx" - container type: :naked - - installer script: { - executable: "pipx", - args: ["install", "--force", "#{staged_path}/algokit-#{version}-py3-none-any.whl"], - print_stderr: false, - } - installer script: { - executable: "pipx", - args: ["ensurepath"], - } - installer script: { - executable: "bash", - args: ["-c", "echo \$(which pipx) uninstall $package_name >#{staged_path}/uninstall.sh"], - } - - uninstall script: { - executable: "bash", - args: ["#{staged_path}/uninstall.sh"], - } + binary "#{staged_path}/#{token}" + + postflight do + set_permissions "#{staged_path}/#{token}", "0755" + end + + uninstall delete: "/usr/local/bin/#{token}" end EOF - if [[ ! -f $ruby ]]; then - >&2 echo "Failed to generate $ruby 🚫" + if [[ ! -f $cask_file ]]; then + >&2 echo "Failed to generate $cask_file 🚫" exit $CASK_GENERATION_FAILED else - echo "Created $ruby 🎉" + echo "Created $cask_file 🎉" fi } create_pr() { - local full_ruby=`realpath $ruby` + local full_cask_filepath=`realpath $cask_file` echo "Cloning $homebrew_tap_repo..." clone_dir=`mktemp -d` git clone "https://oauth2:${TAP_GITHUB_TOKEN}@github.com/${homebrew_tap_repo}.git" $clone_dir - echo "Commiting Casks/$ruby..." + echo "Commiting Casks/$cask_file..." pushd $clone_dir dest_branch="$command-update-$version" git checkout -b $dest_branch mkdir -p $clone_dir/Casks - cp $full_ruby $clone_dir/Casks + cp $full_cask_filepath $clone_dir/Casks message="Updating $command to $version" git add . git commit --message "$message"