diff --git a/.github/workflows/native-build.yml b/.github/workflows/native-build.yml index 4a17e885..d1097d9b 100644 --- a/.github/workflows/native-build.yml +++ b/.github/workflows/native-build.yml @@ -60,6 +60,7 @@ jobs: - name: Replace app.spec with version from workflow branch if: ${{ github.event.inputs.useWorkflowSpec == 'true' }} + shell: bash run: cp temp-spec/app.spec ./app.spec - name: Cache Python venv @@ -78,6 +79,7 @@ jobs: architecture: "x64" - name: Install Python dependencies + shell: bash run: | pip install uv uv venv --system-site-packages --relocatable @@ -97,18 +99,34 @@ jobs: - name: Build frontend working-directory: ui + shell: bash run: | pnpm install --production --prefer-offline --frozen-lockfile pnpm run generate + - name: Clean build outputs (Unix) + if: runner.os != 'Windows' + shell: bash + run: rm -rf build dist release || true + + - name: Clean build outputs (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + if (Test-Path build) { Remove-Item -Recurse -Force build } + if (Test-Path dist) { Remove-Item -Recurse -Force dist } + if (Test-Path release) { Remove-Item -Recurse -Force release } + - name: Build native binary (Windows) if: matrix.os == 'windows-latest' + shell: powershell run: | .\.venv\Scripts\activate uv run pyinstaller ./app.spec - name: Build native binary (Linux/macOS) if: matrix.os != 'windows-latest' + shell: bash run: | . .venv/bin/activate uv run pyinstaller ./app.spec @@ -117,34 +135,45 @@ jobs: uses: actions/upload-artifact@v4 with: name: app-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }} - path: dist/* + path: dist/YTPTube/** - name: Package artifact (Unix) if: startsWith(env.TAG_NAME, 'v') && runner.os != 'Windows' + shell: bash run: | mkdir -p release + PKG_DIR="YTPTube-${{ env.TAG_NAME }}" ZIP_NAME="ytptube-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}.zip" - if [ ! -d dist ] || [ -z "$(ls -A dist)" ]; then - echo "dist directory is empty, skipping packaging." + if [ ! -f "dist/YTPTube/YTPTube" ]; then + echo "Missing dist/YTPTube/YTPTube" + find dist -maxdepth 3 -type f | head -n 200 exit 1 fi - zip -r "release/${ZIP_NAME}" dist/ + rm -rf "dist/${PKG_DIR}" + mv "dist/YTPTube" "dist/${PKG_DIR}" + + zip -r "release/${ZIP_NAME}" "dist/${PKG_DIR}" - name: Package artifact (Windows) if: startsWith(env.TAG_NAME, 'v') && runner.os == 'Windows' shell: powershell run: | + $pkgDir = "YTPTube-${{ env.TAG_NAME }}" $zipName = "ytptube-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}.zip" New-Item -ItemType Directory -Force -Path release | Out-Null - if (!(Test-Path dist) -or !(Get-ChildItem -Path dist)) { - Write-Host "dist directory is empty, skipping packaging." + if (!(Test-Path "dist\YTPTube\YTPTube.exe")) { + Write-Host "Missing dist\YTPTube\YTPTube.exe" + Get-ChildItem -Recurse dist | Select-Object -First 200 | Format-Table -AutoSize exit 1 } - Compress-Archive -Path dist\* -DestinationPath "release\$zipName" + if (Test-Path "dist\$pkgDir") { Remove-Item -Recurse -Force "dist\$pkgDir" } + Move-Item "dist\YTPTube" "dist\$pkgDir" + + Compress-Archive -Path "dist\$pkgDir" -DestinationPath "release\$zipName" -Force - name: Upload to GitHub Release if: startsWith(env.TAG_NAME, 'v') diff --git a/app.spec b/app.spec index 3e33306c..0e783ff9 100644 --- a/app.spec +++ b/app.spec @@ -13,10 +13,6 @@ MANUAL_MAP = { def top_level_modules(dist_name: str) -> list[str]: - """ - Resolve distribution name -> importable top-level modules. - Honors MANUAL_MAP first; falls back to metadata or normalized name. - """ manual: list[str] | None = MANUAL_MAP.get(dist_name) or MANUAL_MAP.get(dist_name.replace("-", "_")) if manual: return manual @@ -24,7 +20,7 @@ def top_level_modules(dist_name: str) -> list[str]: try: dist = importlib.metadata.distribution(dist_name) - top_level = [] + top_level: list[str] = [] for f in dist.files or []: if f.name == "top_level.txt": txt = (dist.locate_file(f)).read_text().splitlines() @@ -44,7 +40,7 @@ def top_level_modules(dist_name: str) -> list[str]: return [dist_name.replace("-", "_")] -def parse_pyproject(path="pyproject.toml") -> set[str]: +def parse_pyproject(path: str = "pyproject.toml") -> set[str]: with open(path, "rb") as f: data = tomllib.load(f) @@ -52,12 +48,11 @@ def parse_pyproject(path="pyproject.toml") -> set[str]: for extra in data["project"].get("optional-dependencies", {}).values(): reqs.update(extra) - # Strip env markers and versions return {re.split(r"[ ;<>=]", r, 1)[0] for r in reqs} # noqa: B034 dist_names = parse_pyproject() -hidden = [] +hidden: list[str] = [] for dist in dist_names: if sys.platform != "win32" and dist in ("python-magic-bin", "tzdata"): @@ -69,14 +64,13 @@ for dist in dist_names: hidden += [ "engineio.async_drivers.aiohttp", "socketio.async_drivers.aiohttp", - "app", - "dotenv" - "socketio", + "socketio", # python-socketio top-level + "engineio", "aiohttp", - "engineio" + "dotenv", + "app", ] -# Deduplicate hidden = sorted(set(hidden)) a = Analysis( # noqa: F821 # type: ignore @@ -90,23 +84,29 @@ a = Analysis( # noqa: F821 # type: ignore hiddenimports=hidden, hookspath=[], runtime_hooks=[], - excludes=["libstdc++.so.6"], + excludes=[], # do NOT exclude libstdc++.so.6 cipher=block_cipher, ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) # type: ignore # noqa: F821 -exe = EXE( # type: ignore # noqa: F821 +exe = EXE( # noqa: F821 # type: ignore pyz, a.scripts, - a.binaries, - a.zipfiles, - a.datas, name="YTPTube", debug=False, strip=False, - upx=True, console=True, icon="ui/public/favicon.ico", - onefile=True, + exclude_binaries=True, +) + +coll = COLLECT( # noqa: F821 # type: ignore + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=False, + name="YTPTube", )