Skip to content

Commit 3c61e8f

Browse files
committed
Fix desktop packaging runtime issues
1 parent 7bf0062 commit 3c61e8f

9 files changed

Lines changed: 168 additions & 21 deletions

File tree

scripts/build_local.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ cd "$PROJECT_ROOT"
1111
python3 -m venv .venv
1212
./.venv/bin/python -m pip install --upgrade pip
1313
./.venv/bin/python -m pip install -r requirements-build.txt
14+
VERSION="$(./.venv/bin/python -c 'from videomerge import __version__; print(__version__)')"
15+
BUNDLE_VERSION="${VERSION%%-*}"
1416

1517
rm -rf build
1618

@@ -33,6 +35,7 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
3335
PYINSTALLER_ARGS+=(
3436
--windowed
3537
--icon "$ICON_ICNS"
38+
--osx-bundle-identifier "com.videomergingtool.app"
3639
--add-binary "$VENDOR_FFMPEG_DIR/ffmpeg:ffmpeg"
3740
--add-binary "$VENDOR_FFMPEG_DIR/ffprobe:ffmpeg"
3841
)
@@ -52,6 +55,10 @@ echo
5255
if [[ "$(uname -s)" == "Darwin" && -d "$PROJECT_ROOT/dist/$NAME.app" ]]; then
5356
DMG_PATH="$PROJECT_ROOT/dist/$NAME.dmg"
5457
DMG_ROOT="$PROJECT_ROOT/build/dmg-root"
58+
INFO_PLIST="$PROJECT_ROOT/dist/$NAME.app/Contents/Info.plist"
59+
plutil -replace CFBundleShortVersionString -string "$BUNDLE_VERSION" "$INFO_PLIST"
60+
plutil -replace CFBundleVersion -string "$VERSION" "$INFO_PLIST"
61+
plutil -replace CFBundleIconName -string "VideoMergingTool" "$INFO_PLIST"
5562
rm -f "$DMG_PATH"
5663
rm -rf "$DMG_ROOT"
5764
mkdir -p "$DMG_ROOT"

scripts/build_windows.ps1

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ if (-not (Test-Path ".venv")) {
1515

1616
& ".\.venv\Scripts\python.exe" -m pip install --upgrade pip
1717
& ".\.venv\Scripts\python.exe" -m pip install -r requirements-build.txt
18+
$Version = (& ".\.venv\Scripts\python.exe" -c "from videomerge import __version__; print(__version__)").Trim()
19+
$VersionFile = Join-Path $ProjectRoot "build\version_info.txt"
1820

1921
if (Test-Path "build") {
2022
Remove-Item -Recurse -Force "build"
2123
}
2224

2325
& ".\.venv\Scripts\python.exe" "scripts\prepare_ffmpeg.py" --output $VendorFfmpegDir --force
26+
& ".\.venv\Scripts\python.exe" "scripts\write_windows_version.py" $VersionFile
2427

2528
& ".\.venv\Scripts\pyinstaller.exe" `
2629
--onefile `
@@ -29,19 +32,21 @@ if (Test-Path "build") {
2932
--noconfirm `
3033
--name $Name `
3134
--icon $IconPath `
35+
--version-file $VersionFile `
3236
--collect-all typer `
3337
--collect-all click `
3438
--collect-all rich `
3539
--collect-all webview `
3640
--collect-all certifi `
3741
--hidden-import videomerge.gui `
3842
--hidden-import tkinter `
39-
--add-binary "$VendorFfmpegDir\ffmpeg.exe;ffmpeg" `
40-
--add-binary "$VendorFfmpegDir\ffprobe.exe;ffmpeg" `
4143
main.py
4244

4345
Write-Host ""
4446
Write-Host "Build complete: $ProjectRoot\dist\$Name.exe"
47+
New-Item -ItemType Directory -Force -Path (Join-Path $ProjectRoot "dist\ffmpeg") | Out-Null
48+
Copy-Item (Join-Path $VendorFfmpegDir "ffmpeg.exe") (Join-Path $ProjectRoot "dist\ffmpeg\ffmpeg.exe") -Force
49+
Copy-Item (Join-Path $VendorFfmpegDir "ffprobe.exe") (Join-Path $ProjectRoot "dist\ffmpeg\ffprobe.exe") -Force
4550

4651
$Inno = Get-Command "ISCC.exe" -ErrorAction SilentlyContinue
4752
if ($Inno) {
@@ -54,7 +59,7 @@ if ($Inno) {
5459
[Setup]
5560
AppId=$AppId
5661
AppName=$Name
57-
AppVersion=1.0.0
62+
AppVersion=$Version
5863
DefaultDirName={autopf}\$Name
5964
DefaultGroupName=$Name
6065
OutputDir=$InstallerDir
@@ -65,6 +70,7 @@ SolidCompression=yes
6570
6671
[Files]
6772
Source: "$ExePath"; DestDir: "{app}"; Flags: ignoreversion
73+
Source: "$ProjectRoot\dist\ffmpeg\*"; DestDir: "{app}\ffmpeg"; Flags: ignoreversion recursesubdirs
6874
6975
[Icons]
7076
Name: "{group}\$Name"; Filename: "{app}\$Name.exe"

scripts/write_windows_version.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import re
5+
import sys
6+
from pathlib import Path
7+
8+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
9+
10+
from videomerge import __version__
11+
12+
13+
def main() -> None:
14+
parser = argparse.ArgumentParser()
15+
parser.add_argument("output", type=Path)
16+
args = parser.parse_args()
17+
18+
numeric = _numeric_version(__version__)
19+
args.output.parent.mkdir(parents=True, exist_ok=True)
20+
args.output.write_text(
21+
f"""# UTF-8
22+
VSVersionInfo(
23+
ffi=FixedFileInfo(
24+
filevers=({numeric[0]}, {numeric[1]}, {numeric[2]}, {numeric[3]}),
25+
prodvers=({numeric[0]}, {numeric[1]}, {numeric[2]}, {numeric[3]}),
26+
mask=0x3f,
27+
flags=0x0,
28+
OS=0x40004,
29+
fileType=0x1,
30+
subtype=0x0,
31+
date=(0, 0)
32+
),
33+
kids=[
34+
StringFileInfo([
35+
StringTable(
36+
'040904B0',
37+
[
38+
StringStruct('CompanyName', 'VideoMergingTool'),
39+
StringStruct('FileDescription', 'VideoMergingTool'),
40+
StringStruct('FileVersion', '{__version__}'),
41+
StringStruct('InternalName', 'VideoMergingTool'),
42+
StringStruct('OriginalFilename', 'VideoMergingTool.exe'),
43+
StringStruct('ProductName', 'VideoMergingTool'),
44+
StringStruct('ProductVersion', '{__version__}')
45+
]
46+
)
47+
]),
48+
VarFileInfo([VarStruct('Translation', [1033, 1200])])
49+
]
50+
)
51+
""",
52+
encoding="utf-8",
53+
)
54+
55+
56+
def _numeric_version(version: str) -> tuple[int, int, int, int]:
57+
parts = [int(part) for part in re.findall(r"\d+", version)[:4]]
58+
while len(parts) < 4:
59+
parts.append(0)
60+
return tuple(parts[:4])
61+
62+
63+
if __name__ == "__main__":
64+
main()

tests/test_gui_folder_picker.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,30 @@
77

88

99
class GuiFolderPickerTests(unittest.TestCase):
10-
def test_windows_uses_tk_folder_picker(self) -> None:
10+
def test_windows_prefers_file_dialog_folder_picker(self) -> None:
1111
with patch("videomerge.gui.platform.system", return_value="Windows"), patch(
12-
"videomerge.gui._pick_folder_tk",
12+
"videomerge.gui._pick_folder_windows",
1313
return_value="C:/Videos",
14-
) as pick_tk, patch("videomerge.gui._pick_folder_macos") as pick_macos:
14+
) as pick_windows, patch("videomerge.gui._pick_folder_tk") as pick_tk, patch(
15+
"videomerge.gui._pick_folder_macos"
16+
) as pick_macos:
1517
selected = _pick_folder("source")
1618

1719
self.assertEqual(selected, "C:/Videos")
18-
pick_tk.assert_called_once_with("Select source video folder")
20+
pick_windows.assert_called_once_with("Select source video folder")
21+
pick_tk.assert_not_called()
1922
pick_macos.assert_not_called()
2023

24+
def test_windows_falls_back_to_tk_folder_picker(self) -> None:
25+
with patch("videomerge.gui.platform.system", return_value="Windows"), patch(
26+
"videomerge.gui._pick_folder_windows",
27+
return_value="",
28+
), patch("videomerge.gui._pick_folder_tk", return_value="C:/Videos") as pick_tk:
29+
selected = _pick_folder("source")
30+
31+
self.assertEqual(selected, "C:/Videos")
32+
pick_tk.assert_called_once_with("Select source video folder")
33+
2134
def test_macos_uses_osascript_folder_picker(self) -> None:
2235
with patch("videomerge.gui.platform.system", return_value="Darwin"), patch(
2336
"videomerge.gui._pick_folder_macos",

videomerge/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__all__ = ["__version__"]
22

3-
__version__ = "0.1.0"
3+
__version__ = "0.2.6-dev"

videomerge/gpu.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from enum import Enum
88

99
from .models import ToolPaths
10+
from .utils import subprocess_window_kwargs
1011

1112

1213
class GpuMode(str, Enum):
@@ -69,15 +70,20 @@ def resolve_gpu_plan(
6970
return GpuPlan(gpu_mode, None, available, reason)
7071

7172

72-
def detect_ffmpeg_encoders(tools: ToolPaths) -> set[str]:
73-
process = subprocess.run(
74-
[str(tools.ffmpeg), "-hide_banner", "-encoders"],
75-
stdout=subprocess.PIPE,
76-
stderr=subprocess.PIPE,
77-
text=True,
78-
encoding="utf-8",
79-
errors="replace",
80-
)
73+
def detect_ffmpeg_encoders(tools: ToolPaths, timeout: int = 5) -> set[str]:
74+
try:
75+
process = subprocess.run(
76+
[str(tools.ffmpeg), "-hide_banner", "-encoders"],
77+
stdout=subprocess.PIPE,
78+
stderr=subprocess.PIPE,
79+
text=True,
80+
encoding="utf-8",
81+
errors="replace",
82+
timeout=timeout,
83+
**subprocess_window_kwargs(),
84+
)
85+
except (OSError, subprocess.TimeoutExpired):
86+
return set()
8187
output = f"{process.stdout}\n{process.stderr}"
8288
encoders: set[str] = set()
8389
for line in output.splitlines():

videomerge/gui.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from .models import MergeMode, Orientation, VideoFile
2323
from .probe import probe_files
2424
from .scanner import scan_video_files
25+
from .utils import subprocess_window_kwargs
26+
from . import __version__
2527

2628

2729
HTML = r"""<!doctype html>
@@ -925,7 +927,7 @@ def _open_desktop_window(url: str) -> None:
925927
"Desktop GUI requires pywebview. Install dependencies with `pip install -r requirements.txt`."
926928
) from exc
927929

928-
webview.create_window("VideoMergingTool", url, width=1280, height=820, min_size=(900, 620))
930+
webview.create_window(f"VideoMergingTool {__version__}", url, width=1280, height=820, min_size=(900, 620))
929931
webview.start()
930932

931933

@@ -969,7 +971,7 @@ def _deps(self) -> None:
969971
try:
970972
logger = _gui_logger(state)
971973
tools = resolve_tools(logger, True, default_tools_dir())
972-
encoders = detect_ffmpeg_encoders(tools)
974+
encoders = detect_ffmpeg_encoders(tools, timeout=3)
973975
gpu_encoders = sorted(
974976
encoder
975977
for encoder in encoders
@@ -1182,6 +1184,7 @@ def _terminate_process(process: subprocess.Popen[str]) -> None:
11821184
stdout=subprocess.DEVNULL,
11831185
stderr=subprocess.DEVNULL,
11841186
check=False,
1187+
**subprocess_window_kwargs(),
11851188
)
11861189
else:
11871190
os.killpg(process.pid, signal.SIGTERM)
@@ -1201,7 +1204,7 @@ def _terminate_process(process: subprocess.Popen[str]) -> None:
12011204

12021205
def _process_group_kwargs() -> dict[str, object]:
12031206
if os.name == "nt":
1204-
return {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}
1207+
return {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP | getattr(subprocess, "CREATE_NO_WINDOW", 0)}
12051208
return {"start_new_session": True}
12061209

12071210

@@ -1220,6 +1223,8 @@ def _pick_folder(kind: str) -> str:
12201223
title = "Select output folder" if kind == "output" else "Select source video folder"
12211224
if platform.system() == "Darwin":
12221225
return _pick_folder_macos(title)
1226+
if platform.system() == "Windows":
1227+
return _pick_folder_windows(title) or _pick_folder_tk(title)
12231228
return _pick_folder_tk(title)
12241229

12251230

@@ -1233,6 +1238,43 @@ def _pick_folder_macos(title: str) -> str:
12331238
text=True,
12341239
encoding="utf-8",
12351240
errors="replace",
1241+
**subprocess_window_kwargs(),
1242+
)
1243+
if result.returncode == 0:
1244+
return result.stdout.strip()
1245+
except Exception:
1246+
return ""
1247+
return ""
1248+
1249+
1250+
def _pick_folder_windows(title: str) -> str:
1251+
escaped_title = title.replace("'", "''")
1252+
script = f"""
1253+
Add-Type -AssemblyName System.Windows.Forms
1254+
$dialog = New-Object System.Windows.Forms.OpenFileDialog
1255+
$dialog.Title = '{escaped_title}'
1256+
$dialog.CheckFileExists = $false
1257+
$dialog.ValidateNames = $false
1258+
$dialog.FileName = 'Select this folder'
1259+
$dialog.Filter = 'Folders|*.folder'
1260+
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {{
1261+
$selected = $dialog.FileName
1262+
if (Test-Path -LiteralPath $selected -PathType Container) {{
1263+
Write-Output $selected
1264+
}} else {{
1265+
Write-Output (Split-Path -Parent $selected)
1266+
}}
1267+
}}
1268+
"""
1269+
try:
1270+
result = subprocess.run(
1271+
["powershell", "-NoProfile", "-STA", "-ExecutionPolicy", "Bypass", "-Command", script],
1272+
stdout=subprocess.PIPE,
1273+
stderr=subprocess.PIPE,
1274+
text=True,
1275+
encoding="utf-8",
1276+
errors="replace",
1277+
**subprocess_window_kwargs(),
12361278
)
12371279
if result.returncode == 0:
12381280
return result.stdout.strip()

videomerge/probe.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from .errors import ProbeError
1010
from .models import Orientation, ToolPaths, VideoFile
11-
from .utils import parse_fraction
11+
from .utils import parse_fraction, subprocess_window_kwargs
1212

1313

1414
def probe_file(path: Path, tools: ToolPaths, logger: logging.Logger) -> VideoFile:
@@ -29,6 +29,7 @@ def probe_file(path: Path, tools: ToolPaths, logger: logging.Logger) -> VideoFil
2929
text=True,
3030
encoding="utf-8",
3131
errors="replace",
32+
**subprocess_window_kwargs(),
3233
)
3334
if process.returncode != 0:
3435
raise ProbeError(process.stderr.strip() or f"ffprobe failed for {path}")

videomerge/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4+
import os
45
import shlex
56
import subprocess
67
from pathlib import Path
@@ -9,6 +10,12 @@
910
from .errors import CommandError
1011

1112

13+
def subprocess_window_kwargs() -> dict[str, int]:
14+
if os.name != "nt":
15+
return {}
16+
return {"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0)}
17+
18+
1219
def run_command(args: Sequence[str | Path], logger: logging.Logger, dry_run: bool = False) -> None:
1320
printable = " ".join(shlex.quote(str(arg)) for arg in args)
1421
logger.debug("Running command: %s", printable)
@@ -23,6 +30,7 @@ def run_command(args: Sequence[str | Path], logger: logging.Logger, dry_run: boo
2330
text=True,
2431
encoding="utf-8",
2532
errors="replace",
33+
**subprocess_window_kwargs(),
2634
)
2735
if process.returncode != 0:
2836
if process.stdout.strip():

0 commit comments

Comments
 (0)