From 74ff568ba45f9bd9bcbc1db538ce1497520efc35 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 27 Aug 2026 14:54:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/packaging/doctor_workstation.spec | 42 +++++++++++++++++++++++-- app/packaging/windows/version_info.txt | 31 ------------------ app/pyproject.toml | 6 ++-- app/scripts/package_macos.sh | 6 ++-- app/scripts/package_windows.ps1 | 23 +++++++------- app/src/doctor_workstation/__init__.py | 3 +- app/tests/test_one_click_entrypoints.py | 27 ++++++++++++++++ app/uv.lock | 3 +- 8 files changed, 89 insertions(+), 52 deletions(-) delete mode 100644 app/packaging/windows/version_info.txt diff --git a/app/packaging/doctor_workstation.spec b/app/packaging/doctor_workstation.spec index b3bf3d4a0..e9080ec1d 100644 --- a/app/packaging/doctor_workstation.spec +++ b/app/packaging/doctor_workstation.spec @@ -11,6 +11,7 @@ their platform media backends. """ import os +import re import sys from pathlib import Path @@ -23,9 +24,46 @@ RESOURCES = PROJECT_ROOT / "resources" WINDOWS_ICON = RESOURCES / "branding" / "app-icon.ico" MACOS_ICON = RESOURCES / "branding" / "app-icon.icns" ENTITLEMENTS = PROJECT_ROOT / "packaging" / "macos" / "entitlements.plist" -VERSION_FILE = PROJECT_ROOT / "packaging" / "windows" / "version_info.txt" +VERSION_SOURCE = SOURCE_ROOT / "doctor_workstation" / "__init__.py" +VERSION_TEMPLATE = PROJECT_ROOT / "packaging" / "windows" / "version_info.template.txt" MEDIA_SMOKE_HOOK = PROJECT_ROOT / "packaging" / "runtime_media_smoke.py" + +def read_application_version(): + source = VERSION_SOURCE.read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', source, re.MULTILINE) + if not match: + raise SystemExit(f"Application version is missing from {VERSION_SOURCE}") + return match.group(1) + + +def windows_version_tuple(version): + match = re.match(r"^(\d+(?:\.\d+){0,3})", version) + if not match: + raise SystemExit(f"Application version is invalid for Windows resources: {version}") + parts = [int(part) for part in match.group(1).split(".")] + return tuple((parts + [0, 0, 0, 0])[:4]) + + +def generate_windows_version_file(version): + template = VERSION_TEMPLATE.read_text(encoding="utf-8") + version_tuple = ", ".join(str(part) for part in windows_version_tuple(version)) + rendered = template.replace("@VERSION_TUPLE@", version_tuple) + rendered = rendered.replace("@VERSION_STRING@", version) + output = PROJECT_ROOT / "build" / "generated" / "version_info.txt" + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(rendered, encoding="utf-8") + return output + + +APP_VERSION = read_application_version() +if sys.platform == "win32": + if not VERSION_TEMPLATE.is_file(): + raise SystemExit(f"Windows version template is missing: {VERSION_TEMPLATE}") + VERSION_FILE = generate_windows_version_file(APP_VERSION) +else: + VERSION_FILE = None + if not ENTRY_POINT.is_file(): raise SystemExit(f"Application entry point is missing: {ENTRY_POINT}") if not (VIDEO_DIST / "index.html").is_file(): @@ -107,7 +145,7 @@ exe = EXE( icon=str(WINDOWS_ICON) if sys.platform == "win32" else None, codesign_identity=os.environ.get("MACOS_CODESIGN_IDENTITY") if is_macos else None, entitlements_file=str(ENTITLEMENTS) if is_macos else None, - version=str(VERSION_FILE) if sys.platform == "win32" else None, + version=str(VERSION_FILE) if VERSION_FILE else None, ) collection = COLLECT( diff --git a/app/packaging/windows/version_info.txt b/app/packaging/windows/version_info.txt deleted file mode 100644 index dd3c7d942..000000000 --- a/app/packaging/windows/version_info.txt +++ /dev/null @@ -1,31 +0,0 @@ -# UTF-8 -# Example PyInstaller version resource. Update all four version tuples together. -VSVersionInfo( - ffi=FixedFileInfo( - filevers=(0, 1, 0, 0), - prodvers=(0, 1, 0, 0), - mask=0x3f, - flags=0x0, - OS=0x40004, - fileType=0x1, - subtype=0x0, - date=(0, 0) - ), - kids=[ - StringFileInfo([ - StringTable( - '080404B0', - [ - StringStruct('CompanyName', 'ZYT'), - StringStruct('FileDescription', '医生工作台'), - StringStruct('FileVersion', '0.1.0.0'), - StringStruct('InternalName', 'DoctorWorkstation'), - StringStruct('OriginalFilename', 'DoctorWorkstation.exe'), - StringStruct('ProductName', '医生工作台'), - StringStruct('ProductVersion', '0.1.0.0') - ] - ) - ]), - VarFileInfo([VarStruct('Translation', [2052, 1200])]) - ] -) diff --git a/app/pyproject.toml b/app/pyproject.toml index 650393524..3ec7f4265 100644 --- a/app/pyproject.toml +++ b/app/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "zhenyang-doctor-workstation" -version = "1.0.0" +dynamic = ["version"] description = "Cross-platform doctor consultation workstation for Windows and macOS" readme = "README.md" requires-python = ">=3.11" @@ -31,6 +31,9 @@ build = [ [project.scripts] doctor-workstation = "doctor_workstation.app:main" +[tool.hatch.version] +path = "src/doctor_workstation/__init__.py" + [tool.hatch.build.targets.wheel] packages = ["src/doctor_workstation"] @@ -46,4 +49,3 @@ target-version = "py311" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM"] ignore = ["E501"] - diff --git a/app/scripts/package_macos.sh b/app/scripts/package_macos.sh index 78bde63e3..853712e88 100755 --- a/app/scripts/package_macos.sh +++ b/app/scripts/package_macos.sh @@ -131,9 +131,9 @@ PYINSTALLER_PYTHON="$python_bin" SKIP_FRONTEND_INSTALL=1 \ /bin/bash "$script_dir/build_macos.sh" artifact="$project_root/dist/DoctorWorkstation.app" -project_version="$(/usr/bin/awk -F '"' '/^version = "/ { print $2; exit }' \ - "$project_root/pyproject.toml")" -[[ -n "$project_version" ]] || macos_die "无法从 pyproject.toml 读取版本号。" +project_version="$(/usr/bin/awk -F '"' '/^__version__ = "/ { print $2; exit }' \ + "$project_root/src/doctor_workstation/__init__.py")" +[[ -n "$project_version" ]] || macos_die "无法从 doctor_workstation.__version__ 读取版本号。" case "$(uname -m)" in arm64) release_arch="arm64" ;; x86_64) release_arch="x64" ;; diff --git a/app/scripts/package_windows.ps1 b/app/scripts/package_windows.ps1 index 6bb078bfe..a8cbb5fad 100644 --- a/app/scripts/package_windows.ps1 +++ b/app/scripts/package_windows.ps1 @@ -20,7 +20,7 @@ $ReleaseLauncher = Join-Path $DistributionRoot "Start_DoctorWorkstation.bat" $InstallerDefinition = Join-Path $ProjectRoot "packaging\windows\doctor_workstation.iss" $EnsureInstallerCompiler = Join-Path $PSScriptRoot "ensure_inno_setup.ps1" $InstallerMessagesFile = Join-Path $ProjectRoot ".build-tools\inno-languages\ChineseSimplified.isl" -$ProjectMetadata = Join-Path $ProjectRoot "pyproject.toml" +$VersionSource = Join-Path $ProjectRoot "src\doctor_workstation\__init__.py" $MediaSmokeHook = Join-Path $ProjectRoot "packaging\runtime_media_smoke.py" $WindowsIcon = Join-Path $ProjectRoot "resources\branding\app-icon.ico" @@ -53,7 +53,7 @@ try { $BuildScript, $PackageLock, (Join-Path $ProjectRoot "uv.lock"), - $ProjectMetadata, + $VersionSource, $MediaSmokeHook, $WindowsIcon, $ReleaseLauncherTemplate, @@ -65,6 +65,16 @@ try { } } + $VersionText = [System.IO.File]::ReadAllText($VersionSource) + $VersionMatch = [regex]::Match( + $VersionText, + '(?m)^\s*__version__\s*=\s*"([^"]+)"' + ) + if (-not $VersionMatch.Success) { + throw "Unable to read the application version from $VersionSource" + } + $ProjectVersion = $VersionMatch.Groups[1].Value + $Npm = Find-Application -Name "npm.cmd" $Node = Find-Application -Name "node.exe" if (-not $Npm -or -not $Node) { @@ -156,15 +166,6 @@ try { throw "Build completed without the expected executable: $Executable" } - $ProjectText = [System.IO.File]::ReadAllText($ProjectMetadata) - $VersionMatch = [regex]::Match( - $ProjectText, - '(?m)^\s*version\s*=\s*"([^"]+)"' - ) - if (-not $VersionMatch.Success) { - throw "Unable to read the project version from $ProjectMetadata" - } - $ProjectVersion = $VersionMatch.Groups[1].Value $ReleaseZip = Join-Path $DistributionRoot ( "DoctorWorkstation-Windows-x64-$ProjectVersion.zip" ) diff --git a/app/src/doctor_workstation/__init__.py b/app/src/doctor_workstation/__init__.py index e1f905f79..904b2b5b2 100644 --- a/app/src/doctor_workstation/__init__.py +++ b/app/src/doctor_workstation/__init__.py @@ -2,4 +2,5 @@ __all__ = ["__version__"] -__version__ = "0.1.0" +# Single source of truth for runtime, package, installer, and executable versions. +__version__ = "1.1.0" diff --git a/app/tests/test_one_click_entrypoints.py b/app/tests/test_one_click_entrypoints.py index 2c4d8347a..156978ba7 100644 --- a/app/tests/test_one_click_entrypoints.py +++ b/app/tests/test_one_click_entrypoints.py @@ -1,7 +1,10 @@ from __future__ import annotations +import tomllib from pathlib import Path +from doctor_workstation import __version__ + PROJECT_ROOT = Path(__file__).resolve().parents[1] @@ -9,6 +12,30 @@ def read(relative_path: str) -> str: return (PROJECT_ROOT / relative_path).read_text(encoding="utf-8") +def test_application_version_has_one_source() -> None: + pyproject = tomllib.loads(read("pyproject.toml")) + project = pyproject["project"] + + assert "version" not in project + assert "version" in project["dynamic"] + assert pyproject["tool"]["hatch"]["version"]["path"] == ( + "src/doctor_workstation/__init__.py" + ) + assert __version__ + + version_template = read("packaging/windows/version_info.template.txt") + assert "@VERSION_TUPLE@" in version_template + assert "@VERSION_STRING@" in version_template + assert "0.1.0" not in version_template + + spec = read("packaging/doctor_workstation.spec") + windows_package_script = read("scripts/package_windows.ps1") + macos_package_script = read("scripts/package_macos.sh") + assert 'VERSION_SOURCE = SOURCE_ROOT / "doctor_workstation" / "__init__.py"' in spec + assert 'src\\doctor_workstation\\__init__.py' in windows_package_script + assert 'src/doctor_workstation/__init__.py' in macos_package_script + + def test_windows_one_click_entrypoints_and_release_pipeline() -> None: for name in ( "Run_DoctorWorkstation.bat", diff --git a/app/uv.lock b/app/uv.lock index 7baa696d0..c01961c3a 100644 --- a/app/uv.lock +++ b/app/uv.lock @@ -746,7 +746,6 @@ wheels = [ [[package]] name = "zhenyang-doctor-workstation" -version = "0.1.0" source = { editable = "." } dependencies = [ { name = "httpx" }, @@ -778,7 +777,7 @@ requires-dist = [ { name = "python-dotenv", specifier = ">=1.0.1,<2" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9,<1" }, ] -provides-extras = ["dev", "build"] +provides-extras = ["build", "dev"] [[package]] name = "zipp"