更新
This commit is contained in:
@@ -11,6 +11,7 @@ their platform media backends.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -23,9 +24,46 @@ RESOURCES = PROJECT_ROOT / "resources"
|
|||||||
WINDOWS_ICON = RESOURCES / "branding" / "app-icon.ico"
|
WINDOWS_ICON = RESOURCES / "branding" / "app-icon.ico"
|
||||||
MACOS_ICON = RESOURCES / "branding" / "app-icon.icns"
|
MACOS_ICON = RESOURCES / "branding" / "app-icon.icns"
|
||||||
ENTITLEMENTS = PROJECT_ROOT / "packaging" / "macos" / "entitlements.plist"
|
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"
|
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():
|
if not ENTRY_POINT.is_file():
|
||||||
raise SystemExit(f"Application entry point is missing: {ENTRY_POINT}")
|
raise SystemExit(f"Application entry point is missing: {ENTRY_POINT}")
|
||||||
if not (VIDEO_DIST / "index.html").is_file():
|
if not (VIDEO_DIST / "index.html").is_file():
|
||||||
@@ -107,7 +145,7 @@ exe = EXE(
|
|||||||
icon=str(WINDOWS_ICON) if sys.platform == "win32" else None,
|
icon=str(WINDOWS_ICON) if sys.platform == "win32" else None,
|
||||||
codesign_identity=os.environ.get("MACOS_CODESIGN_IDENTITY") if is_macos else None,
|
codesign_identity=os.environ.get("MACOS_CODESIGN_IDENTITY") if is_macos else None,
|
||||||
entitlements_file=str(ENTITLEMENTS) 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(
|
collection = COLLECT(
|
||||||
|
|||||||
@@ -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])])
|
|
||||||
]
|
|
||||||
)
|
|
||||||
+4
-2
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "zhenyang-doctor-workstation"
|
name = "zhenyang-doctor-workstation"
|
||||||
version = "1.0.0"
|
dynamic = ["version"]
|
||||||
description = "Cross-platform doctor consultation workstation for Windows and macOS"
|
description = "Cross-platform doctor consultation workstation for Windows and macOS"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
@@ -31,6 +31,9 @@ build = [
|
|||||||
[project.scripts]
|
[project.scripts]
|
||||||
doctor-workstation = "doctor_workstation.app:main"
|
doctor-workstation = "doctor_workstation.app:main"
|
||||||
|
|
||||||
|
[tool.hatch.version]
|
||||||
|
path = "src/doctor_workstation/__init__.py"
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["src/doctor_workstation"]
|
packages = ["src/doctor_workstation"]
|
||||||
|
|
||||||
@@ -46,4 +49,3 @@ target-version = "py311"
|
|||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "I", "UP", "B", "SIM"]
|
select = ["E", "F", "I", "UP", "B", "SIM"]
|
||||||
ignore = ["E501"]
|
ignore = ["E501"]
|
||||||
|
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ PYINSTALLER_PYTHON="$python_bin" SKIP_FRONTEND_INSTALL=1 \
|
|||||||
/bin/bash "$script_dir/build_macos.sh"
|
/bin/bash "$script_dir/build_macos.sh"
|
||||||
|
|
||||||
artifact="$project_root/dist/DoctorWorkstation.app"
|
artifact="$project_root/dist/DoctorWorkstation.app"
|
||||||
project_version="$(/usr/bin/awk -F '"' '/^version = "/ { print $2; exit }' \
|
project_version="$(/usr/bin/awk -F '"' '/^__version__ = "/ { print $2; exit }' \
|
||||||
"$project_root/pyproject.toml")"
|
"$project_root/src/doctor_workstation/__init__.py")"
|
||||||
[[ -n "$project_version" ]] || macos_die "无法从 pyproject.toml 读取版本号。"
|
[[ -n "$project_version" ]] || macos_die "无法从 doctor_workstation.__version__ 读取版本号。"
|
||||||
case "$(uname -m)" in
|
case "$(uname -m)" in
|
||||||
arm64) release_arch="arm64" ;;
|
arm64) release_arch="arm64" ;;
|
||||||
x86_64) release_arch="x64" ;;
|
x86_64) release_arch="x64" ;;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ $ReleaseLauncher = Join-Path $DistributionRoot "Start_DoctorWorkstation.bat"
|
|||||||
$InstallerDefinition = Join-Path $ProjectRoot "packaging\windows\doctor_workstation.iss"
|
$InstallerDefinition = Join-Path $ProjectRoot "packaging\windows\doctor_workstation.iss"
|
||||||
$EnsureInstallerCompiler = Join-Path $PSScriptRoot "ensure_inno_setup.ps1"
|
$EnsureInstallerCompiler = Join-Path $PSScriptRoot "ensure_inno_setup.ps1"
|
||||||
$InstallerMessagesFile = Join-Path $ProjectRoot ".build-tools\inno-languages\ChineseSimplified.isl"
|
$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"
|
$MediaSmokeHook = Join-Path $ProjectRoot "packaging\runtime_media_smoke.py"
|
||||||
$WindowsIcon = Join-Path $ProjectRoot "resources\branding\app-icon.ico"
|
$WindowsIcon = Join-Path $ProjectRoot "resources\branding\app-icon.ico"
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ try {
|
|||||||
$BuildScript,
|
$BuildScript,
|
||||||
$PackageLock,
|
$PackageLock,
|
||||||
(Join-Path $ProjectRoot "uv.lock"),
|
(Join-Path $ProjectRoot "uv.lock"),
|
||||||
$ProjectMetadata,
|
$VersionSource,
|
||||||
$MediaSmokeHook,
|
$MediaSmokeHook,
|
||||||
$WindowsIcon,
|
$WindowsIcon,
|
||||||
$ReleaseLauncherTemplate,
|
$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"
|
$Npm = Find-Application -Name "npm.cmd"
|
||||||
$Node = Find-Application -Name "node.exe"
|
$Node = Find-Application -Name "node.exe"
|
||||||
if (-not $Npm -or -not $Node) {
|
if (-not $Npm -or -not $Node) {
|
||||||
@@ -156,15 +166,6 @@ try {
|
|||||||
throw "Build completed without the expected executable: $Executable"
|
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 (
|
$ReleaseZip = Join-Path $DistributionRoot (
|
||||||
"DoctorWorkstation-Windows-x64-$ProjectVersion.zip"
|
"DoctorWorkstation-Windows-x64-$ProjectVersion.zip"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
|
|
||||||
__all__ = ["__version__"]
|
__all__ = ["__version__"]
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
# Single source of truth for runtime, package, installer, and executable versions.
|
||||||
|
__version__ = "1.1.0"
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tomllib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from doctor_workstation import __version__
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
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")
|
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:
|
def test_windows_one_click_entrypoints_and_release_pipeline() -> None:
|
||||||
for name in (
|
for name in (
|
||||||
"Run_DoctorWorkstation.bat",
|
"Run_DoctorWorkstation.bat",
|
||||||
|
|||||||
Generated
+1
-2
@@ -746,7 +746,6 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zhenyang-doctor-workstation"
|
name = "zhenyang-doctor-workstation"
|
||||||
version = "0.1.0"
|
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
@@ -778,7 +777,7 @@ requires-dist = [
|
|||||||
{ name = "python-dotenv", specifier = ">=1.0.1,<2" },
|
{ name = "python-dotenv", specifier = ">=1.0.1,<2" },
|
||||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9,<1" },
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9,<1" },
|
||||||
]
|
]
|
||||||
provides-extras = ["dev", "build"]
|
provides-extras = ["build", "dev"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zipp"
|
name = "zipp"
|
||||||
|
|||||||
Reference in New Issue
Block a user