From 34f2b8627674a9ee4f12328744c05ffe10d4c89c Mon Sep 17 00:00:00 2001 From: philippe leprince Date: Wed, 9 Sep 2026 17:29:39 +0200 Subject: [PATCH] feat(build): add portable Windows packaging target Add a `portable` CMake target that stages a relocatable installation, removes development-only artifacts, and creates a ZIP archive. Include launcher and usage documentation, package validation, dependency collection, and Windows build guidance. Signed-off-by: philippe leprince --- CMakeLists.txt | 6 ++ cmake/portable.cmake | 62 +++++++++++++++++ docs/reference/build_guides/windows.md | 17 +++++ scripts/portable/README.txt.in | 19 ++++++ scripts/portable/portable.cmake.in | 94 ++++++++++++++++++++++++++ scripts/portable/xstudio.bat.in | 4 ++ 6 files changed, 202 insertions(+) create mode 100644 cmake/portable.cmake create mode 100644 scripts/portable/README.txt.in create mode 100644 scripts/portable/portable.cmake.in create mode 100644 scripts/portable/xstudio.bat.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 10fa0f481..58a1d20f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -374,3 +374,9 @@ if (WIN32) include(CPack) endif() + +if (WIN32) + # Portable (no-install) package target. Must come last so its dependency + # collection sees every target defined above. + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/portable.cmake) +endif() diff --git a/cmake/portable.cmake b/cmake/portable.cmake new file mode 100644 index 000000000..9069df834 --- /dev/null +++ b/cmake/portable.cmake @@ -0,0 +1,62 @@ +# Windows portable (no-install) package. +# +# `cmake --build --target portable` produces: +# /portable/xSTUDIO--win64/ staged, relocatable folder +# /xSTUDIO--win64-portable.zip archive with one top-level folder +# +# Included from the top-level CMakeLists.txt (Windows only), after every +# add_subdirectory() call so the target collection below sees all targets. +# The staging/prune/zip work lives in scripts/portable/portable.cmake.in, +# configured into the build tree as portable.cmake. + +set(XSTUDIO_PORTABLE_NAME "xSTUDIO-${XSTUDIO_GLOBAL_VERSION}-win64") +set(XSTUDIO_PORTABLE_STAGE "${CMAKE_BINARY_DIR}/portable/${XSTUDIO_PORTABLE_NAME}") + +configure_file(scripts/portable/portable.cmake.in + ${CMAKE_BINARY_DIR}/portable.cmake @ONLY) + +add_custom_target(portable + COMMAND ${CMAKE_COMMAND} -E rm -rf "${XSTUDIO_PORTABLE_STAGE}" + COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" + --prefix "${XSTUDIO_PORTABLE_STAGE}" --config $ + COMMAND ${CMAKE_COMMAND} -P "${CMAKE_BINARY_DIR}/portable.cmake" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + COMMENT "Building portable xSTUDIO package" + USES_TERMINAL VERBATIM) + +# Make `portable` depend on every buildsystem target so it builds the same +# set as `--target package`. ALL membership is not enough: targets such as +# python_module and *_COPY_QML are add_custom_target(... ALL ...) and only +# run when explicitly requested or depended upon. Targets without ALL +# membership (Qt qmllint/qmlcachegen helpers, clang-tidy, clangformat, +# `portable` itself) report EXCLUDE_FROM_ALL and are skipped; the denylist is +# a belt-and-braces guard for the always-defined lint targets. +set(XSTUDIO_PORTABLE_DENYLIST portable clang-tidy clangformat) + +function(_xstudio_portable_collect_targets dir) + get_directory_property(_targets DIRECTORY "${dir}" BUILDSYSTEM_TARGETS) + foreach(_target IN LISTS _targets) + get_target_property(_excluded "${_target}" EXCLUDE_FROM_ALL) + if(_excluded) + continue() + endif() + get_target_property(_type "${_target}" TYPE) + if(_type STREQUAL "INTERFACE_LIBRARY") + continue() + endif() + list(FIND XSTUDIO_PORTABLE_DENYLIST "${_target}" _denied) + if(_denied GREATER_EQUAL 0) + continue() + endif() + set_property(GLOBAL APPEND PROPERTY _XSTUDIO_PORTABLE_TARGETS "${_target}") + endforeach() + + get_directory_property(_subdirs DIRECTORY "${dir}" SUBDIRECTORIES) + foreach(_subdir IN LISTS _subdirs) + _xstudio_portable_collect_targets("${_subdir}") + endforeach() +endfunction() + +_xstudio_portable_collect_targets("${CMAKE_SOURCE_DIR}") +get_property(_xstudio_portable_targets GLOBAL PROPERTY _XSTUDIO_PORTABLE_TARGETS) +add_dependencies(portable ${_xstudio_portable_targets}) diff --git a/docs/reference/build_guides/windows.md b/docs/reference/build_guides/windows.md index f519ae7d5..50654dc12 100644 --- a/docs/reference/build_guides/windows.md +++ b/docs/reference/build_guides/windows.md @@ -90,6 +90,23 @@ RelWithDebInfo and Debug variants are also available — see [CMakePresets.json] If the build is successful, you should have an executable in the 'build' folder called something like 'xSTUDIO-1.2.0-win64.exe'. This can be executed to start the xSTUDIO installer. +### Portable build (no installer) + +As an alternative to the NSIS installer you can build a relocatable, no-install folder plus a zip archive: + + cmake --build build --target portable + +This produces: + +- `build/portable/xSTUDIO--win64/` - the staged package (kept for inspection), run it via `xstudio.bat` or `bin\xstudio.exe` +- `build/xSTUDIO--win64-portable.zip` - the same folder as a single archive + +Notes: + +- Like `--target package`, the `portable` target re-runs the full install including `windeployqt`, so it is about as slow. +- The folder is relocatable, but not data-isolated: preferences, autosaves and thumbnails are still written under the Windows user profile (see the package's `README.txt` for the exact paths). +- `.xst` file associations and Start-menu entries are installer-only and are not part of the portable package. + ### Running xSTUDIO from the build tree (dev workflow) For a quick dev run without going through the installer, the build generates a launcher at `build/run_xstudio.bat`. Arguments are forwarded to xstudio: diff --git a/scripts/portable/README.txt.in b/scripts/portable/README.txt.in new file mode 100644 index 000000000..87b26d790 --- /dev/null +++ b/scripts/portable/README.txt.in @@ -0,0 +1,19 @@ +xSTUDIO @XSTUDIO_GLOBAL_VERSION@ - portable build (Windows x64) +=============================================================== + +No installation required: run xstudio.bat (or bin\xstudio.exe) directly. +The folder is fully relocatable - move or unpack it anywhere and run it. + +Note that this build is relocatable but not data-isolated. Preferences, +autosaves and thumbnail caches are still written under the Windows user +profile, shared with any other xSTUDIO installation: + + %USERPROFILE%\.config\DNEG\xstudio\preferences + %USERPROFILE%\xStudio\autosave + %USERPROFILE%\xStudio\thumbnails + +.xst session-file associations and Start-menu entries are created by the +NSIS installer only; they are not part of this portable package. + +Documentation is available from the Help menu inside xSTUDIO +(share/xstudio/docs). diff --git a/scripts/portable/portable.cmake.in b/scripts/portable/portable.cmake.in new file mode 100644 index 000000000..fe3a4fa4e --- /dev/null +++ b/scripts/portable/portable.cmake.in @@ -0,0 +1,94 @@ +# Generated from scripts/portable/portable.cmake.in by cmake/portable.cmake. +# Runs as `cmake -P` after the full install has been staged at +# @XSTUDIO_PORTABLE_STAGE@. Sanity-checks, prunes dev-only content, adds the +# launcher/README and zips the result. + +set(XSTUDIO_PORTABLE_STAGE "@XSTUDIO_PORTABLE_STAGE@") +set(XSTUDIO_PORTABLE_NAME "@XSTUDIO_PORTABLE_NAME@") +set(XSTUDIO_GLOBAL_VERSION "@XSTUDIO_GLOBAL_VERSION@") +set(CMAKE_BINARY_DIR "@CMAKE_BINARY_DIR@") +set(CMAKE_SOURCE_DIR "@CMAKE_SOURCE_DIR@") + +# 1. Sanity check the staged install before touching anything. +set(_required_paths + "${XSTUDIO_PORTABLE_STAGE}/bin/xstudio.exe" + "${XSTUDIO_PORTABLE_STAGE}/bin/python3/python.exe" + "${XSTUDIO_PORTABLE_STAGE}/bin/Qt6Core.dll" + "${XSTUDIO_PORTABLE_STAGE}/bin/qml" + "${XSTUDIO_PORTABLE_STAGE}/share/xstudio/plugin") +foreach(_path IN LISTS _required_paths) + if(NOT EXISTS "${_path}") + message(FATAL_ERROR + "Portable packaging failed: '${_path}' is missing from the staged " + "install at '${XSTUDIO_PORTABLE_STAGE}'. Build the project fully " + "before running the 'portable' target.") + endif() +endforeach() + +# 2. Guard: on Windows share/xstudio/lib should only receive import libs +# (cmake/macros.cmake default_options -> LIBRARY DESTINATION share/xstudio/lib). +# Refuse to prune it if it ever contains runtime DLLs. +file(GLOB _lib_dlls "${XSTUDIO_PORTABLE_STAGE}/share/xstudio/lib/*.dll") +if(_lib_dlls) + message(FATAL_ERROR + "Portable packaging aborted: '${XSTUDIO_PORTABLE_STAGE}/share/xstudio/lib' " + "contains runtime DLLs: ${_lib_dlls}. The portable step prunes that " + "directory because on Windows it is expected to hold only import libs. " + "Update scripts/portable/portable.cmake.in if this layout change is intended.") +endif() + +# Same guard for lib/: on Windows it receives only CMake package config and +# import libs (ARCHIVE default destination of install(TARGETS ... EXPORT)). +file(GLOB _root_lib_dlls "${XSTUDIO_PORTABLE_STAGE}/lib/*.dll") +if(_root_lib_dlls) + message(FATAL_ERROR + "Portable packaging aborted: '${XSTUDIO_PORTABLE_STAGE}/lib' contains " + "runtime DLLs: ${_root_lib_dlls}. The portable step prunes that directory " + "because on Windows it is expected to hold only dev artifacts. Update " + "scripts/portable/portable.cmake.in if this layout change is intended.") +endif() + +# 3. Prune dev-only content. Kept on purpose: *.pdb (crash triage), python/ +# (root-level .py copy), share/xstudio/docs (Help menu), bin/python3 (the *.lib +# files inside it belong to the embedded python/numpy and stay as well). +set(_prune_paths + "${XSTUDIO_PORTABLE_STAGE}/include" + "${XSTUDIO_PORTABLE_STAGE}/extern" + "${XSTUDIO_PORTABLE_STAGE}/lib" + "${XSTUDIO_PORTABLE_STAGE}/share/xstudio/lib") +foreach(_path IN LISTS _prune_paths) + if(EXISTS "${_path}") + file(REMOVE_RECURSE "${_path}") + endif() +endforeach() + +file(GLOB _dev_artifacts + "${XSTUDIO_PORTABLE_STAGE}/bin/*.lib" + "${XSTUDIO_PORTABLE_STAGE}/bin/*.exp") +if(_dev_artifacts) + file(REMOVE ${_dev_artifacts}) +endif() + +# 4. Launcher (start avoids leaving a console window on double-click, +# arguments are forwarded). +configure_file("${CMAKE_SOURCE_DIR}/scripts/portable/xstudio.bat.in" + "${XSTUDIO_PORTABLE_STAGE}/xstudio.bat" @ONLY) + +# 5. README. +configure_file("${CMAKE_SOURCE_DIR}/scripts/portable/README.txt.in" + "${XSTUDIO_PORTABLE_STAGE}/README.txt" @ONLY) + +# 6. Zip with a single top-level folder inside. +set(_portable_zip "${CMAKE_BINARY_DIR}/${XSTUDIO_PORTABLE_NAME}-portable.zip") +execute_process( + COMMAND "${CMAKE_COMMAND}" -E tar cf "${_portable_zip}" --format=zip "${XSTUDIO_PORTABLE_NAME}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/portable" + RESULT_VARIABLE _zip_result) +if(NOT _zip_result EQUAL 0) + message(FATAL_ERROR + "Portable packaging failed: could not create '${_portable_zip}' " + "(exit code ${_zip_result}).") +endif() + +message(STATUS "Portable xSTUDIO staged at: ${XSTUDIO_PORTABLE_STAGE}") +message(STATUS "Portable xSTUDIO zip: ${_portable_zip}") diff --git a/scripts/portable/xstudio.bat.in b/scripts/portable/xstudio.bat.in new file mode 100644 index 000000000..23d373b0c --- /dev/null +++ b/scripts/portable/xstudio.bat.in @@ -0,0 +1,4 @@ +@echo off +REM Portable launcher for xSTUDIO. Run xstudio.exe from the bin folder +REM next to this file; arguments are forwarded. Generated by CMake, do not edit. +start "" "%~dp0bin\xstudio.exe" %*