diff --git a/.github/workflows/build_executable.yml b/.github/workflows/build_executable.yml index a0a0e706..0de165b6 100644 --- a/.github/workflows/build_executable.yml +++ b/.github/workflows/build_executable.yml @@ -11,10 +11,17 @@ permissions: jobs: build: + name: Build on ${{ matrix.os }} (${{ matrix.mode }} mode) strategy: fail-fast: false matrix: - os: [ ubuntu-20.04, macos-11, windows-2019 ] + os: [ ubuntu-20.04, macos-11, macos-13-xlarge, windows-2019 ] + mode: [ 'onefile', 'onedir' ] + exclude: + - os: ubuntu-20.04 + mode: onedir + - os: windows-2019 + mode: onedir runs-on: ${{ matrix.os }} @@ -48,17 +55,17 @@ jobs: git checkout $LATEST_TAG echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV - - name: Set up Python 3.7 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: - python-version: '3.7' + python-version: '3.12' - name: Load cached Poetry setup id: cached-poetry uses: actions/cache@v3 with: path: ~/.local - key: poetry-${{ matrix.os }}-0 # increment to reset cache + key: poetry-${{ matrix.os }}-1 # increment to reset cache - name: Setup Poetry if: steps.cached-poetry.outputs.cache-hit != 'true' @@ -70,15 +77,9 @@ jobs: run: echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Install dependencies - run: poetry install + run: poetry install --without dev,test - - name: Build executable - run: poetry run pyinstaller pyinstaller.spec - - - name: Test executable - run: ./dist/cycode version - - - name: Sign macOS executable + - name: Import macOS signing certificate if: runner.os == 'macOS' env: APPLE_CERT: ${{ secrets.APPLE_CERT }} @@ -100,8 +101,25 @@ jobs: security import $CERTIFICATE_PATH -P "$APPLE_CERT_PWD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security list-keychain -d user -s $KEYCHAIN_PATH - # sign executable - codesign --deep --force --options=runtime --entitlements entitlements.plist --sign "$APPLE_CERT_NAME" --timestamp dist/cycode + - name: Build executable (onefile) + if: matrix.mode == 'onefile' + env: + APPLE_CERT_NAME: ${{ secrets.APPLE_CERT_NAME }} + run: | + poetry run pyinstaller pyinstaller.spec + echo "PATH_TO_CYCODE_CLI_EXECUTABLE=dist/cycode-cli" >> $GITHUB_ENV + + - name: Build executable (onedir) + if: matrix.mode == 'onedir' + env: + CYCODE_ONEDIR_MODE: 1 + APPLE_CERT_NAME: ${{ secrets.APPLE_CERT_NAME }} + run: | + poetry run pyinstaller pyinstaller.spec + echo "PATH_TO_CYCODE_CLI_EXECUTABLE=dist/cycode-cli/cycode-cli" >> $GITHUB_ENV + + - name: Test executable + run: time $PATH_TO_CYCODE_CLI_EXECUTABLE version - name: Notarize macOS executable if: runner.os == 'macOS' @@ -114,17 +132,20 @@ jobs: xcrun notarytool store-credentials "notarytool-profile" --apple-id "$APPLE_NOTARIZATION_EMAIL" --team-id "$APPLE_NOTARIZATION_TEAM_ID" --password "$APPLE_NOTARIZATION_PWD" # create zip file (notarization does not support binaries) - ditto -c -k --keepParent dist/cycode notarization.zip + ditto -c -k --keepParent dist/cycode-cli notarization.zip # notarize app (this will take a while) xcrun notarytool submit notarization.zip --keychain-profile "notarytool-profile" --wait - # we can't staple the app because it's executable. we should only staple app bundles like .dmg - # xcrun stapler staple dist/cycode + # we can't staple the app because it's executable - name: Test macOS signed executable if: runner.os == 'macOS' - run: ./dist/cycode version + run: | + time $PATH_TO_CYCODE_CLI_EXECUTABLE version + + # verify signature + codesign -dv --verbose=4 $PATH_TO_CYCODE_CLI_EXECUTABLE - name: Import cert for Windows and setup envs if: runner.os == 'Windows' @@ -155,39 +176,57 @@ jobs: smksp_cert_sync.exe :: sign executable - signtool.exe sign /sha1 %SM_CODE_SIGNING_CERT_SHA1_HASH% /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ".\dist\cycode.exe" + signtool.exe sign /sha1 %SM_CODE_SIGNING_CERT_SHA1_HASH% /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ".\dist\cycode-cli.exe" - name: Test Windows signed executable if: runner.os == 'Windows' shell: cmd run: | :: call executable and expect correct output - .\dist\cycode.exe version + .\dist\cycode-cli.exe version :: verify signature - signtool.exe verify /v /pa ".\dist\cycode.exe" + signtool.exe verify /v /pa ".\dist\cycode-cli.exe" - name: Prepare files on Windows if: runner.os == 'Windows' run: | echo "ARTIFACT_NAME=cycode-win" >> $GITHUB_ENV - mv dist/cycode.exe dist/cycode-win.exe + mv dist/cycode-cli.exe dist/cycode-win.exe powershell -Command "(Get-FileHash -Algorithm SHA256 dist/cycode-win.exe).Hash" > sha256 head -c 64 sha256 > dist/cycode-win.exe.sha256 - - name: Prepare files on macOS - if: runner.os == 'macOS' + - name: Prepare files on Intel macOS (onefile) + if: runner.os == 'macOS' && runner.arch == 'X64' && matrix.mode == 'onefile' run: | echo "ARTIFACT_NAME=cycode-mac" >> $GITHUB_ENV - mv dist/cycode dist/cycode-mac + mv dist/cycode-cli dist/cycode-mac shasum -a 256 dist/cycode-mac > sha256 head -c 64 sha256 > dist/cycode-mac.sha256 + - name: Prepare files on Apple Silicon macOS (onefile) + if: runner.os == 'macOS' && runner.arch == 'ARM64' && matrix.mode == 'onefile' + run: | + echo "ARTIFACT_NAME=cycode-mac-arm" >> $GITHUB_ENV + mv dist/cycode-cli dist/cycode-mac-arm + shasum -a 256 dist/cycode-mac-arm > sha256 + head -c 64 sha256 > dist/cycode-mac-arm.sha256 + + - name: Prepare files on Intel macOS (onedir) + if: runner.os == 'macOS' && runner.arch == 'X64' && matrix.mode == 'onedir' + run: | + echo "ARTIFACT_NAME=cycode-mac-onedir" >> $GITHUB_ENV + + - name: Prepare files on Apple Silicon macOS (onedir) + if: runner.os == 'macOS' && runner.arch == 'ARM64' && matrix.mode == 'onedir' + run: | + echo "ARTIFACT_NAME=cycode-mac-arm-onedir" >> $GITHUB_ENV + - name: Prepare files on Linux if: runner.os == 'Linux' run: | echo "ARTIFACT_NAME=cycode-linux" >> $GITHUB_ENV - mv dist/cycode dist/cycode-linux + mv dist/cycode-cli dist/cycode-linux sha256sum dist/cycode-linux > sha256 head -c 64 sha256 > dist/cycode-linux.sha256 diff --git a/.github/workflows/tests_full.yml b/.github/workflows/tests_full.yml index aaa58d89..8225b0c3 100644 --- a/.github/workflows/tests_full.yml +++ b/.github/workflows/tests_full.yml @@ -64,10 +64,9 @@ jobs: run: poetry install - name: Run executable test - if: runner.os == 'Linux' run: | poetry run pyinstaller pyinstaller.spec - ./dist/cycode version + ./dist/cycode-cli version - name: Run pytest run: poetry run pytest diff --git a/poetry.lock b/poetry.lock index 46d5ffcd..e4e96a4d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -92,13 +92,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.7.22" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] [[package]] @@ -326,13 +326,13 @@ packaging = ">=20.9" [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -372,13 +372,13 @@ test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] @@ -508,13 +508,13 @@ files = [ [[package]] name = "platformdirs" -version = "3.11.0" +version = "4.0.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, ] [package.dependencies] @@ -935,4 +935,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.7,<3.13" -content-hash = "016a02b0698698558aa590693fdda4a0ce558e247da4ed1eaf7b9315881575f6" +content-hash = "9c77c886972dc6da818e14a175507a87fe32c773824ef40ee3fab9c0725e9fe4" diff --git a/pyinstaller.spec b/pyinstaller.spec index 6409682a..cb3382d4 100644 --- a/pyinstaller.spec +++ b/pyinstaller.spec @@ -1,64 +1,48 @@ # -*- mode: python ; coding: utf-8 -*- # Run `poetry run pyinstaller pyinstaller.spec` to generate the binary. +# Set the env var `CYCODE_ONEDIR_MODE` to generate a single directory instead of a single file. - -block_cipher = None - -INIT_FILE_PATH = os.path.join('cycode', '__init__.py') +_INIT_FILE_PATH = os.path.join('cycode', '__init__.py') +_CODESIGN_IDENTITY = os.environ.get('APPLE_CERT_NAME') +_ONEDIR_MODE = os.environ.get('CYCODE_ONEDIR_MODE') is not None # save the prev content of __init__ file -with open(INIT_FILE_PATH, 'r', encoding='UTF-8') as file: +with open(_INIT_FILE_PATH, 'r', encoding='UTF-8') as file: prev_content = file.read() import dunamai as _dunamai + VERSION_PLACEHOLDER = '0.0.0' CLI_VERSION = _dunamai.get_version('cycode', first_choice=_dunamai.Version.from_git).serialize( metadata=False, bump=True, style=_dunamai.Style.Pep440 ) -# write version from Git Tag to freeze the value and don't depend on Git -with open(INIT_FILE_PATH, 'w', encoding='UTF-8') as file: +# write the version from Git Tag to freeze the value and don't depend on Git +with open(_INIT_FILE_PATH, 'w', encoding='UTF-8') as file: file.write(prev_content.replace(VERSION_PLACEHOLDER, CLI_VERSION)) a = Analysis( - ['cycode/cli/main.py'], - pathex=[], - binaries=[], + scripts=['cycode/cli/main.py'], datas=[('cycode/cli/config.yaml', 'cycode/cli'), ('cycode/cyclient/config.yaml', 'cycode/cyclient')], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], excludes=['tests'], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False, ) -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe_args = [PYZ(a.pure, a.zipped_data), a.scripts, a.binaries, a.zipfiles, a.datas] +if _ONEDIR_MODE: + exe_args = [PYZ(a.pure), a.scripts] exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='cycode', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, + *exe_args, + name='cycode-cli', + exclude_binaries=bool(_ONEDIR_MODE), target_arch=None, - codesign_identity=None, - entitlements_file=None, + codesign_identity=_CODESIGN_IDENTITY, + entitlements_file='entitlements.plist', ) +if _ONEDIR_MODE: + coll = COLLECT(exe, a.binaries, a.datas, name='cycode-cli') + # rollback the prev content of the __init__ file -with open(INIT_FILE_PATH, 'w', encoding='UTF-8') as file: +with open(_INIT_FILE_PATH, 'w', encoding='UTF-8') as file: file.write(prev_content) diff --git a/pyproject.toml b/pyproject.toml index b0057fd1..272e3bb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ coverage = ">=7.2.3,<7.3.0" responses = ">=0.23.1,<0.24.0" [tool.poetry.group.executable.dependencies] -pyinstaller = ">=5.13.0,<5.14.0" +pyinstaller = ">=5.13.2,<5.14.0" dunamai = ">=1.18.0,<1.19.0" [tool.poetry.group.dev.dependencies]