From 764c5c5073adf2b5411224926bc38a3ee60e2141 Mon Sep 17 00:00:00 2001 From: xryu Date: Fri, 24 Apr 2026 11:51:32 +0800 Subject: [PATCH] fix: package Windows decode runtime artifacts --- .github/workflows/ci.yml | 38 ++++++++++++ .github/workflows/release.yml | 7 +++ crates/dsview-core/src/lib.rs | 33 +++++++++- crates/dsview-sys/build.rs | 4 ++ .../compat/msvc_decode_preinclude.h | 9 +++ crates/dsview-sys/native/CMakeLists.txt | 13 ++++ .../native/windows/dsview_decode_runtime.def | 20 +++++++ crates/dsview-sys/src/lib.rs | 57 +++++++++++++++++- tools/package-bundle.py | 60 +++++++++++++++++++ tools/validate-bundle.py | 9 +++ 10 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 crates/dsview-sys/native/windows/dsview_decode_runtime.def diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85705fa..3ea2218 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,13 @@ jobs: with: targets: ${{ matrix.target }} + - name: Set up Python + if: contains(matrix.target, 'windows') + uses: actions/setup-python@v6 + with: + python-version: '3.13' + architecture: ${{ matrix.msvc_arch }} + - name: Initialize MSVC developer command prompt if: contains(matrix.target, 'windows') uses: ilammy/msvc-dev-cmd@v1 @@ -137,6 +144,19 @@ jobs: } "runtime-lib=$runtime" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + - name: Find decode runtime library (Windows) + if: matrix.package_bundle && contains(matrix.target, 'windows') + id: find-decode-runtime-windows + shell: pwsh + run: | + $runtime = Get-ChildItem "target/${{ matrix.target }}/release/build" -Recurse -Filter "dsview_decode_runtime.dll" -ErrorAction Stop | + Sort-Object FullName | + Select-Object -First 1 -ExpandProperty FullName + if (-not $runtime) { + throw "Windows decode runtime library was not found under target/${{ matrix.target }}/release/build" + } + "runtime-lib=$runtime" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + - name: Find runtime library (Unix) if: matrix.package_bundle && !contains(matrix.target, 'windows') id: find-runtime-unix @@ -153,6 +173,22 @@ jobs: fi echo "runtime-lib=$RUNTIME_LIB" >> "$GITHUB_OUTPUT" + - name: Find decode runtime library (Unix) + if: matrix.package_bundle && !contains(matrix.target, 'windows') + id: find-decode-runtime-unix + shell: bash + run: | + if [[ "${{ matrix.target }}" == *"darwin"* ]] || [[ "${{ matrix.target }}" == *"macos"* ]]; then + RUNTIME_LIB=$(find target/${{ matrix.target }}/release/build/dsview-sys-*/out/source-decode-runtime-build -name "libdsview_decode_runtime.dylib" -print -quit) + else + RUNTIME_LIB=$(find target/${{ matrix.target }}/release/build/dsview-sys-*/out/source-decode-runtime-build -name "libdsview_decode_runtime.so" -print -quit) + fi + if [[ -z "$RUNTIME_LIB" ]]; then + echo "decode runtime library was not found for ${{ matrix.target }}" >&2 + exit 1 + fi + echo "runtime-lib=$RUNTIME_LIB" >> "$GITHUB_OUTPUT" + - name: Determine executable path if: matrix.package_bundle id: exe-path @@ -172,3 +208,5 @@ jobs: version: ${{ steps.version.outputs.version }} exe-path: ${{ steps.exe-path.outputs.exe }} runtime-path: ${{ contains(matrix.target, 'windows') && steps.find-runtime-windows.outputs.runtime-lib || steps.find-runtime-unix.outputs.runtime-lib }} + decode-runtime-path: ${{ contains(matrix.target, 'windows') && steps.find-decode-runtime-windows.outputs.runtime-lib || steps.find-decode-runtime-unix.outputs.runtime-lib }} + decoder-dir: DSView/libsigrokdecode4DSL/decoders diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d44b0e2..9953a4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,13 @@ jobs: with: targets: ${{ matrix.target }} + - name: Set up Python + if: contains(matrix.target, 'windows') + uses: actions/setup-python@v6 + with: + python-version: '3.13' + architecture: ${{ matrix.msvc_arch }} + - name: Initialize MSVC developer command prompt if: contains(matrix.target, 'windows') uses: ilammy/msvc-dev-cmd@v1 diff --git a/crates/dsview-core/src/lib.rs b/crates/dsview-core/src/lib.rs index 65c8baf..80c306f 100644 --- a/crates/dsview-core/src/lib.rs +++ b/crates/dsview-core/src/lib.rs @@ -59,6 +59,7 @@ const BUNDLED_RUNTIME_DIR: &str = "runtime"; const BUNDLED_RESOURCE_DIR: &str = "resources"; const BUNDLED_DECODE_RUNTIME_DIR: &str = "decode-runtime"; const BUNDLED_DECODER_DIR: &str = "decoders"; +const BUNDLED_PYTHON_DIR: &str = "python"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SelectionHandle(NonZeroU64); @@ -1221,6 +1222,7 @@ pub struct RuntimeDiscoveryPaths { pub struct DecodeDiscoveryPaths { pub runtime_library: PathBuf, pub decoder_dir: PathBuf, + pub python_home: Option, } impl RuntimeDiscoveryPaths { @@ -1305,6 +1307,7 @@ impl DecodeDiscoveryPaths { .join(BUNDLED_DECODE_RUNTIME_DIR) .join(decode_runtime_library_name()); let bundled_decoder_dir = executable_dir.join(BUNDLED_DECODER_DIR); + let bundled_python_home = executable_dir.join(BUNDLED_PYTHON_DIR); let runtime_library = if let Some(path) = runtime_override { path @@ -1329,11 +1332,21 @@ impl DecodeDiscoveryPaths { developer_decoder_dir() }; + let python_home = if cfg!(windows) + && runtime_library == bundled_runtime + && bundled_python_home.is_dir() + { + Some(bundled_python_home) + } else { + None + }; + ensure_decoder_script_dir(&decoder_dir)?; Ok(Self { runtime_library, decoder_dir, + python_home, }) } } @@ -1778,18 +1791,30 @@ impl DecodeDiscovery { pub fn connect( library_path: impl AsRef, decoder_dir: impl AsRef, + ) -> Result { + Self::connect_with_python_home(library_path, decoder_dir, None::<&Path>) + } + + pub fn connect_with_python_home( + library_path: impl AsRef, + decoder_dir: impl AsRef, + python_home: Option>, ) -> Result { let library_path = library_path.as_ref().to_path_buf(); let decoder_dir = decoder_dir.as_ref().to_path_buf(); + let python_home = python_home.map(|path| path.as_ref().to_path_buf()); ensure_decoder_script_dir(&decoder_dir)?; let runtime = DecodeRuntimeBridge::load(&library_path).map_err(DecodeBringUpError::Runtime)?; - runtime.init(&decoder_dir).map_err(DecodeBringUpError::Runtime)?; + runtime + .init_with_python_home(&decoder_dir, python_home.as_deref()) + .map_err(DecodeBringUpError::Runtime)?; Ok(Self { runtime, paths: DecodeDiscoveryPaths { runtime_library: library_path, decoder_dir, + python_home, }, }) } @@ -1799,7 +1824,11 @@ impl DecodeDiscovery { decoder_dir_override: Option>, ) -> Result { let paths = DecodeDiscoveryPaths::discover(runtime_override, decoder_dir_override)?; - Self::connect(&paths.runtime_library, &paths.decoder_dir) + Self::connect_with_python_home( + &paths.runtime_library, + &paths.decoder_dir, + paths.python_home.as_deref(), + ) } pub fn discovery_paths( diff --git a/crates/dsview-sys/build.rs b/crates/dsview-sys/build.rs index 8b0cb11..084a79b 100644 --- a/crates/dsview-sys/build.rs +++ b/crates/dsview-sys/build.rs @@ -160,6 +160,10 @@ fn main() { "cargo:rerun-if-changed={}", native_root.join("windows/dsview_runtime.def").display() ); + println!( + "cargo:rerun-if-changed={}", + native_root.join("windows/dsview_decode_runtime.def").display() + ); println!( "cargo:rerun-if-changed={}", compat_root.join("msvc_preinclude.h").display() diff --git a/crates/dsview-sys/compat/msvc_decode_preinclude.h b/crates/dsview-sys/compat/msvc_decode_preinclude.h index 0d0afe7..8957c81 100644 --- a/crates/dsview-sys/compat/msvc_decode_preinclude.h +++ b/crates/dsview-sys/compat/msvc_decode_preinclude.h @@ -10,6 +10,15 @@ #define NOMINMAX 1 #endif +/* + * libsigrokdecode4DSL still carries GCC-style attributes in a few source + * files. MSVC rejects these declarations unless they are normalized away + * before the upstream sources are compiled. + */ +#ifndef __attribute__ +#define __attribute__(x) +#endif + #include #include #include diff --git a/crates/dsview-sys/native/CMakeLists.txt b/crates/dsview-sys/native/CMakeLists.txt index 457eeca..ca2a3b9 100644 --- a/crates/dsview-sys/native/CMakeLists.txt +++ b/crates/dsview-sys/native/CMakeLists.txt @@ -211,10 +211,23 @@ if(DSVIEW_BUILD_DECODE_RUNTIME) Python3::Python ) + if(WIN32 AND DEFINED Python3_LIBRARY_RELEASE) + get_filename_component(PYTHON3_LIBRARY_DIR "${Python3_LIBRARY_RELEASE}" DIRECTORY) + target_link_directories(dsview_decode_runtime PRIVATE + "${PYTHON3_LIBRARY_DIR}" + ) + endif() + target_link_directories(dsview_decode_runtime PRIVATE ${GLIB_LIBRARY_DIRS} ) + if(MSVC) + set_target_properties(dsview_decode_runtime PROPERTIES + LINK_FLAGS "/DEF:${DSVIEW_NATIVE_WINDOWS_ROOT}/dsview_decode_runtime.def" + ) + endif() + set_target_properties(dsview_decode_runtime PROPERTIES OUTPUT_NAME dsview_decode_runtime) configure_runtime_target(dsview_decode_runtime msvc_decode_preinclude.h) endif() diff --git a/crates/dsview-sys/native/windows/dsview_decode_runtime.def b/crates/dsview-sys/native/windows/dsview_decode_runtime.def new file mode 100644 index 0000000..6c128a8 --- /dev/null +++ b/crates/dsview-sys/native/windows/dsview_decode_runtime.def @@ -0,0 +1,20 @@ +LIBRARY dsview_decode_runtime +EXPORTS + srd_init + srd_exit + srd_decoder_list + srd_decoder_get_by_id + srd_decoder_load_all + srd_searchpaths_get + srd_strerror + srd_strerror_name + srd_session_new + srd_session_metadata_set + srd_session_start + srd_session_send + srd_session_end + srd_session_destroy + srd_pd_output_callback_add + srd_inst_new + srd_inst_channel_set_all + srd_inst_stack diff --git a/crates/dsview-sys/src/lib.rs b/crates/dsview-sys/src/lib.rs index c969112..db4d801 100644 --- a/crates/dsview-sys/src/lib.rs +++ b/crates/dsview-sys/src/lib.rs @@ -3,8 +3,9 @@ //! This crate is the only allowed home for unsafe FFI when Phase 1 adds //! bindings to `DSView/libsigrok4DSL`. -use std::cell::Cell; -use std::ffi::{CStr, CString}; +use std::cell::{Cell, RefCell}; +use std::env; +use std::ffi::{CStr, CString, OsString}; use std::fmt; use std::fs; use std::os::raw::{c_char, c_int}; @@ -1867,6 +1868,7 @@ impl Drop for RuntimeBridge { pub struct DecodeRuntimeBridge { library_path: PathBuf, initialized: Cell, + python_home_guard: RefCell>, } impl DecodeRuntimeBridge { @@ -1885,6 +1887,7 @@ impl DecodeRuntimeBridge { 0 => Ok(Self { library_path: path.to_path_buf(), initialized: Cell::new(false), + python_home_guard: RefCell::new(None), }), DSVIEW_BRIDGE_ERR_ARG | DSVIEW_DECODE_ERR_ARG => Err(DecodeRuntimeError::InvalidArgument( "decode runtime library path must not be empty".to_string(), @@ -1911,10 +1914,24 @@ impl DecodeRuntimeBridge { } pub fn init(&self, decoder_dir: impl AsRef) -> Result<(), DecodeRuntimeError> { + self.init_with_python_home(decoder_dir, None::<&Path>) + } + + pub fn init_with_python_home( + &self, + decoder_dir: impl AsRef, + python_home: Option>, + ) -> Result<(), DecodeRuntimeError> { + let guard = if let Some(path) = python_home { + Some(PythonHomeGuard::activate(path.as_ref())?) + } else { + None + }; let c_path = path_to_decode_cstring(decoder_dir.as_ref())?; decode_native_call_status("decode runtime init", unsafe { dsview_decode_runtime_init(c_path.as_ptr()) })?; + *self.python_home_guard.borrow_mut() = guard; self.initialized.set(true); Ok(()) } @@ -1924,6 +1941,7 @@ impl DecodeRuntimeBridge { dsview_decode_runtime_exit() })?; self.initialized.set(false); + self.python_home_guard.borrow_mut().take(); Ok(()) } @@ -2002,6 +2020,41 @@ impl Drop for DecodeRuntimeBridge { let _ = dsview_decode_runtime_exit(); }; } + self.python_home_guard.get_mut().take(); + } +} + +#[derive(Debug)] +struct PythonHomeGuard { + previous_home: Option, +} + +impl PythonHomeGuard { + fn activate(path: &Path) -> Result { + if !path.is_dir() { + return Err(DecodeRuntimeError::InvalidArgument(format!( + "python home path does not exist: {}", + path.display() + ))); + } + + let previous_home = env::var_os("PYTHONHOME"); + unsafe { + env::set_var("PYTHONHOME", path); + } + Ok(Self { previous_home }) + } +} + +impl Drop for PythonHomeGuard { + fn drop(&mut self) { + unsafe { + if let Some(previous_home) = &self.previous_home { + env::set_var("PYTHONHOME", previous_home); + } else { + env::remove_var("PYTHONHOME"); + } + } } } diff --git a/tools/package-bundle.py b/tools/package-bundle.py index ae1c1e8..22d45db 100644 --- a/tools/package-bundle.py +++ b/tools/package-bundle.py @@ -67,6 +67,14 @@ def should_skip_decoder_path(path: Path) -> bool: return any(part == "__pycache__" for part in path.parts) or path.suffix in {".pyc", ".pyo"} +def should_skip_python_path(path: Path) -> bool: + return ( + any(part == "__pycache__" for part in path.parts) + or path.suffix in {".pyc", ".pyo"} + or path.parts[:1] == ("Lib",) and len(path.parts) > 1 and path.parts[1] == "site-packages" + ) + + def add_directory(archive: tarfile.TarFile, source: Path, destination: str) -> None: archive.add(source, arcname=destination, recursive=False) for child in sorted(source.rglob("*")): @@ -75,6 +83,19 @@ def add_directory(archive: tarfile.TarFile, source: Path, destination: str) -> N archive.add(child, arcname=f"{destination}/{child.relative_to(source)}", recursive=False) +def add_directory_filtered( + archive: tarfile.TarFile, + source: Path, + destination: str, + skip_predicate, +) -> None: + archive.add(source, arcname=destination, recursive=False) + for child in sorted(source.rglob("*")): + if skip_predicate(child.relative_to(source)): + continue + archive.add(child, arcname=f"{destination}/{child.relative_to(source)}", recursive=False) + + def vcpkg_triplet_for_target(target: str) -> str: if "windows" not in target: raise ValueError(f"target is not a Windows target: {target}") @@ -112,6 +133,44 @@ def windows_dependency_dlls(target: str, runtime_name: str) -> list[Path]: return dlls +def windows_python_runtime_root() -> Path: + root = Path(sys.base_exec_prefix) + ensure_directory(root, "Windows Python runtime root") + return root + + +def windows_python_runtime_dlls() -> list[Path]: + root = windows_python_runtime_root() + dlls = sorted( + { + *root.glob("python*.dll"), + *root.glob("vcruntime*.dll"), + } + ) + if not dlls: + raise FileNotFoundError(f"No Python runtime DLLs were found under {root}") + return dlls + + +def add_windows_python_runtime(archive: tarfile.TarFile, archive_root: str) -> None: + python_root = windows_python_runtime_root() + + for dll in windows_python_runtime_dlls(): + add_file(archive, dll, f"{archive_root}/{dll.name}") + + lib_dir = python_root / "Lib" + ensure_directory(lib_dir, "Windows Python Lib directory") + add_directory_filtered(archive, lib_dir, f"{archive_root}/python/Lib", should_skip_python_path) + + dll_dir = python_root / "DLLs" + if dll_dir.is_dir(): + add_directory_filtered(archive, dll_dir, f"{archive_root}/python/DLLs", should_skip_python_path) + + stdlib_zip = python_root / f"python{sys.version_info.major}{sys.version_info.minor}.zip" + if stdlib_zip.is_file(): + add_file(archive, stdlib_zip, f"{archive_root}/python/{stdlib_zip.name}") + + def main() -> int: args = parse_args() @@ -148,6 +207,7 @@ def main() -> int: dependency, f"{archive_root}/{dependency.name}", ) + add_windows_python_runtime(archive, archive_root) for resource_name in required_resources: resource_path = args.resources / resource_name diff --git a/tools/validate-bundle.py b/tools/validate-bundle.py index ecea86e..1a0f9bc 100644 --- a/tools/validate-bundle.py +++ b/tools/validate-bundle.py @@ -102,6 +102,15 @@ def main() -> int: if "windows" in args.target: for dependency in expected_windows_runtime_dependencies(): require_exists(bundle_root / dependency, "Windows runtime dependency") + if not any(bundle_root.glob("python*.dll")): + raise FileNotFoundError("Bundled Windows Python runtime DLLs were not found") + python_home = bundle_root / "python" + if not python_home.is_dir(): + raise FileNotFoundError("python/ directory not found") + if not (python_home / "Lib").is_dir() and not any(python_home.glob("python*.zip")): + raise FileNotFoundError( + "Bundled Windows Python runtime is missing both Lib/ and pythonXY.zip" + ) runtime_dir = bundle_root / "runtime" if not runtime_dir.is_dir():