Refactor: switch native builds to onedir

This commit is contained in:
arabcoders 2025-12-27 20:46:05 +03:00
parent 9ba68dcfaf
commit 54f0f89dc8
2 changed files with 56 additions and 27 deletions

View file

@ -60,6 +60,7 @@ jobs:
- name: Replace app.spec with version from workflow branch - name: Replace app.spec with version from workflow branch
if: ${{ github.event.inputs.useWorkflowSpec == 'true' }} if: ${{ github.event.inputs.useWorkflowSpec == 'true' }}
shell: bash
run: cp temp-spec/app.spec ./app.spec run: cp temp-spec/app.spec ./app.spec
- name: Cache Python venv - name: Cache Python venv
@ -78,6 +79,7 @@ jobs:
architecture: "x64" architecture: "x64"
- name: Install Python dependencies - name: Install Python dependencies
shell: bash
run: | run: |
pip install uv pip install uv
uv venv --system-site-packages --relocatable uv venv --system-site-packages --relocatable
@ -97,18 +99,34 @@ jobs:
- name: Build frontend - name: Build frontend
working-directory: ui working-directory: ui
shell: bash
run: | run: |
pnpm install --production --prefer-offline --frozen-lockfile pnpm install --production --prefer-offline --frozen-lockfile
pnpm run generate 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) - name: Build native binary (Windows)
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
shell: powershell
run: | run: |
.\.venv\Scripts\activate .\.venv\Scripts\activate
uv run pyinstaller ./app.spec uv run pyinstaller ./app.spec
- name: Build native binary (Linux/macOS) - name: Build native binary (Linux/macOS)
if: matrix.os != 'windows-latest' if: matrix.os != 'windows-latest'
shell: bash
run: | run: |
. .venv/bin/activate . .venv/bin/activate
uv run pyinstaller ./app.spec uv run pyinstaller ./app.spec
@ -117,34 +135,45 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: app-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }} name: app-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}
path: dist/* path: dist/YTPTube/**
- name: Package artifact (Unix) - name: Package artifact (Unix)
if: startsWith(env.TAG_NAME, 'v') && runner.os != 'Windows' if: startsWith(env.TAG_NAME, 'v') && runner.os != 'Windows'
shell: bash
run: | run: |
mkdir -p release mkdir -p release
PKG_DIR="YTPTube-${{ env.TAG_NAME }}"
ZIP_NAME="ytptube-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}.zip" ZIP_NAME="ytptube-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}.zip"
if [ ! -d dist ] || [ -z "$(ls -A dist)" ]; then if [ ! -f "dist/YTPTube/YTPTube" ]; then
echo "dist directory is empty, skipping packaging." echo "Missing dist/YTPTube/YTPTube"
find dist -maxdepth 3 -type f | head -n 200
exit 1 exit 1
fi 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) - name: Package artifact (Windows)
if: startsWith(env.TAG_NAME, 'v') && runner.os == 'Windows' if: startsWith(env.TAG_NAME, 'v') && runner.os == 'Windows'
shell: powershell shell: powershell
run: | run: |
$pkgDir = "YTPTube-${{ env.TAG_NAME }}"
$zipName = "ytptube-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}.zip" $zipName = "ytptube-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}.zip"
New-Item -ItemType Directory -Force -Path release | Out-Null New-Item -ItemType Directory -Force -Path release | Out-Null
if (!(Test-Path dist) -or !(Get-ChildItem -Path dist)) { if (!(Test-Path "dist\YTPTube\YTPTube.exe")) {
Write-Host "dist directory is empty, skipping packaging." Write-Host "Missing dist\YTPTube\YTPTube.exe"
Get-ChildItem -Recurse dist | Select-Object -First 200 | Format-Table -AutoSize
exit 1 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 - name: Upload to GitHub Release
if: startsWith(env.TAG_NAME, 'v') if: startsWith(env.TAG_NAME, 'v')

View file

@ -13,10 +13,6 @@ MANUAL_MAP = {
def top_level_modules(dist_name: str) -> list[str]: 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("-", "_")) manual: list[str] | None = MANUAL_MAP.get(dist_name) or MANUAL_MAP.get(dist_name.replace("-", "_"))
if manual: if manual:
return manual return manual
@ -24,7 +20,7 @@ def top_level_modules(dist_name: str) -> list[str]:
try: try:
dist = importlib.metadata.distribution(dist_name) dist = importlib.metadata.distribution(dist_name)
top_level = [] top_level: list[str] = []
for f in dist.files or []: for f in dist.files or []:
if f.name == "top_level.txt": if f.name == "top_level.txt":
txt = (dist.locate_file(f)).read_text().splitlines() 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("-", "_")] 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: with open(path, "rb") as f:
data = tomllib.load(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(): for extra in data["project"].get("optional-dependencies", {}).values():
reqs.update(extra) reqs.update(extra)
# Strip env markers and versions
return {re.split(r"[ ;<>=]", r, 1)[0] for r in reqs} # noqa: B034 return {re.split(r"[ ;<>=]", r, 1)[0] for r in reqs} # noqa: B034
dist_names = parse_pyproject() dist_names = parse_pyproject()
hidden = [] hidden: list[str] = []
for dist in dist_names: for dist in dist_names:
if sys.platform != "win32" and dist in ("python-magic-bin", "tzdata"): if sys.platform != "win32" and dist in ("python-magic-bin", "tzdata"):
@ -69,14 +64,13 @@ for dist in dist_names:
hidden += [ hidden += [
"engineio.async_drivers.aiohttp", "engineio.async_drivers.aiohttp",
"socketio.async_drivers.aiohttp", "socketio.async_drivers.aiohttp",
"app", "socketio", # python-socketio top-level
"dotenv" "engineio",
"socketio",
"aiohttp", "aiohttp",
"engineio" "dotenv",
"app",
] ]
# Deduplicate
hidden = sorted(set(hidden)) hidden = sorted(set(hidden))
a = Analysis( # noqa: F821 # type: ignore a = Analysis( # noqa: F821 # type: ignore
@ -90,23 +84,29 @@ a = Analysis( # noqa: F821 # type: ignore
hiddenimports=hidden, hiddenimports=hidden,
hookspath=[], hookspath=[],
runtime_hooks=[], runtime_hooks=[],
excludes=["libstdc++.so.6"], excludes=[], # do NOT exclude libstdc++.so.6
cipher=block_cipher, cipher=block_cipher,
) )
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) # type: ignore # noqa: F821 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, pyz,
a.scripts, a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name="YTPTube", name="YTPTube",
debug=False, debug=False,
strip=False, strip=False,
upx=True,
console=True, console=True,
icon="ui/public/favicon.ico", 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",
) )