@@ -261,6 +261,56 @@ def _split_version(version):
261261 return version .split ('.' )
262262
263263
264+ def _find_vs_dev_cmd ():
265+ # Try vswhere first, fall back to common install locations
266+ try :
267+ vswhere = shutil .which ('vswhere' )
268+ if vswhere :
269+ inst = subprocess .check_output ([vswhere , '-latest' , '-products' , '*' ,
270+ '-requires' , 'Microsoft.Component.MSBuild' ,
271+ '-property' , 'installationPath' ], text = True ).strip ()
272+ if inst :
273+ cand = Path (inst ) / 'Common7' / 'Tools' / 'VsDevCmd.bat'
274+ if cand .exists ():
275+ return str (cand )
276+ except subprocess .CalledProcessError :
277+ pass
278+
279+ # Fallback common paths
280+ program_files = [os .environ .get ('ProgramFiles(x86)' ), os .environ .get ('ProgramFiles' )]
281+ for pfdir in program_files :
282+ for vs in sorted (Path (pfdir ).glob ('Microsoft Visual Studio/*/*' ), reverse = True ):
283+ cand = vs / 'Common7' / 'Tools' / 'VsDevCmd.bat'
284+ if cand .exists ():
285+ return str (cand )
286+
287+ raise Error ('Visual Studio developer command script not found. Install VS or add vswhere to PATH.' )
288+
289+
290+ def _capture_vs_env (vs_cmd , arch = 'amd64' ):
291+ """
292+ Run the VS developer batch script in a cmd shell and capture the environment it sets.
293+ Returns a dict suitable for passing as subprocess env or for updating os.environ.
294+ """
295+ # Use cmd.exe to run the batch file and then dump the environment with `set`
296+ try :
297+ out = subprocess .run (f'cmd /c "{ vs_cmd } " -arch={ arch } -no_logo && set' , capture_output = True , text = True )
298+ except subprocess .CalledProcessError as e :
299+ raise Error ('Failed to run Visual Studio dev script: %s' , e .output or str (e ))
300+
301+ env = {}
302+ for line in out .stdout .splitlines ():
303+ k , v = line .split ('=' , 1 )
304+ if v is not None :
305+ env [k ] = v
306+
307+ # VS has plenty of environment variables, so this is a basic sanity check
308+ if len (env ) < 10 :
309+ raise Error ('Failed to capture environment from Visual Studio dev script.' )
310+
311+ return env
312+
313+
264314###########################################################################################
265315# CLI Commands
266316###########################################################################################
@@ -285,7 +335,7 @@ class Check(Command):
285335
286336 @classmethod
287337 def setup_arg_parser (cls , parser : argparse .ArgumentParser ):
288- parser .add_argument ('-v' , '-- version' , help = 'Release version number or name.' )
338+ parser .add_argument ('version' , help = 'Release version number or name.' )
289339 parser .add_argument ('-s' , '--src-dir' , help = 'Source directory.' , default = '.' )
290340 parser .add_argument ('-b' , '--release-branch' , help = 'Release source branch (default: inferred from --version).' )
291341
@@ -507,6 +557,12 @@ def setup_arg_parser(cls, parser: argparse.ArgumentParser):
507557 parser .add_argument ('-p' , '--platform-target' , help = 'Build target platform (default: %(default)s).' ,
508558 choices = ['x86_64' , 'aarch64' ], default = platform .uname ().machine )
509559 parser .add_argument ('-a' , '--appimage' , help = 'Build an AppImage.' , action = 'store_true' )
560+ elif sys .platform == 'win32' :
561+ parser .add_argument ('-p' , '--platform-target' , help = 'Build target platform (default: %(default)s).' ,
562+ choices = ['amd64' , 'arm64' ], default = 'amd64' )
563+ parser .add_argument ('--sign' , help = 'Sign binaries prior to packaging.' , action = 'store_true' )
564+ parser .add_argument ('--sign-cert' , help = 'SHA1 fingerprint of the signing certificate (optional).' )
565+ parser .set_defaults (cmake_generator = 'Ninja' , no_source_tarball = True )
510566
511567 parser .add_argument ('-c' , '--cmake-opts' , nargs = argparse .REMAINDER ,
512568 help = 'Additional CMake options (no other arguments can be specified after this).' )
@@ -530,6 +586,7 @@ def run(self, version, src_dir, output_dir, tag_name, snapshot, no_source_tarbal
530586 '-DCMAKE_INSTALL_PREFIX=' + kwargs ['install_prefix' ],
531587 '-DWITH_TESTS=OFF' ,
532588 '-DWITH_GUI_TESTS=OFF' ,
589+ '-DX_VCPKG_APPLOCAL_DEPS_INSTALL=ON' ,
533590 ]
534591 if not kwargs ['use_system_deps' ] and not kwargs .get ('docker_image' ):
535592 cmake_opts .append (f'-DCMAKE_TOOLCHAIN_FILE={ self ._get_vcpkg_toolchain_file ()} ' )
@@ -567,7 +624,11 @@ def run(self, version, src_dir, output_dir, tag_name, snapshot, no_source_tarbal
567624 def _get_vcpkg_toolchain_file (path = None ):
568625 vcpkg = shutil .which ('vcpkg' , path = path )
569626 if not vcpkg :
570- raise Error ('vcpkg not found in PATH (use --use-system-deps to build with system dependencies instead).' )
627+ # Check the VCPKG_ROOT environment variable
628+ if 'VCPKG_ROOT' in os .environ :
629+ vcpkg = Path (os .environ ['VCPKG_ROOT' ]) / 'vcpkg'
630+ else :
631+ raise Error ('vcpkg not found in PATH (use --use-system-deps to build with system dependencies instead).' )
571632 toolchain = Path (vcpkg ).parent / 'scripts' / 'buildsystems' / 'vcpkg.cmake'
572633 if not toolchain .is_file ():
573634 raise Error ('Toolchain file not found in vcpkg installation directory.' )
@@ -605,11 +666,38 @@ def build_source_tarball(self, version, tag_name, src_dir, output_dir):
605666 _run ([comp , '-6' , '--force' , str (output_file .absolute ())], cwd = src_dir )
606667
607668 # noinspection PyMethodMayBeStatic
608- def build_windows (self , version , src_dir , output_dir , * , parallelism , cmake_opts , ** _ ):
609- pass
669+ def build_windows (self , version , src_dir , output_dir , * , parallelism , cmake_opts , platform_target , sign , sign_cert , ** _ ):
670+ # Check for required tools
671+ if not _cmd_exists ('candle.exe' ) or not _cmd_exists ('light.exe' ) or not _cmd_exists ('heat.exe' ):
672+ raise Error ('WiX Toolset not found on the PATH (candle.exe, light.exe, heat.exe).' )
673+
674+ # Setup build signing if requested
675+ if sign :
676+ cmake_opts .append ('-DWITH_XC_SIGNINSTALL=ON' )
677+ cmake_opts .append (f'-DWITH_XC_SIGNINSTALL_CERT={ sign_cert } ' )
678+
679+ # Find Visual Studio and capture build environment
680+ vs_cmd = _find_vs_dev_cmd ()
681+ vs_env = _capture_vs_env (vs_cmd , arch = platform_target )
682+
683+ # Start the build
684+ with tempfile .TemporaryDirectory () as build_dir :
685+ # NOTE: Shell must be True on Windows to run the command in the provided vs_env
686+ logger .info ('Configuring build...' )
687+ _run (['cmake' , * cmake_opts , str (src_dir )], cwd = build_dir , env = vs_env , shell = True , capture_output = False )
688+
689+ logger .info ('Compiling sources...' )
690+ _run (['cmake' , '--build' , '.' , f'--parallel' , str (parallelism )], cwd = build_dir , env = vs_env , shell = True , capture_output = False )
691+
692+ logger .info ('Packaging application...' )
693+ _run (['cpack' , '-G' , 'ZIP;WIX' ], cwd = build_dir , env = vs_env , shell = True , capture_output = False )
694+
695+ output_files = list (Path (build_dir ).glob ("*.zip" )) + list (Path (build_dir ).glob ("*.msi" ))
696+ for output_file in output_files :
697+ output_file .rename (output_dir / output_file .name )
610698
611699 # noinspection PyMethodMayBeStatic
612- def build_macos (self , version , src_dir , output_dir , * , use_system_deps , parallelism , cmake_opts ,
700+ def build_macos (self , version , src_dir , output_dir , * , use_system_deps , parallelism , cmake_opts ,
613701 macos_target , platform_target , ** _ ):
614702 if not use_system_deps :
615703 cmake_opts .append (f'-DVCPKG_TARGET_TRIPLET={ platform_target .replace ("86_" , "" )} -osx-dynamic-release' )
@@ -763,7 +851,18 @@ def run(self, file, identity, src_dir, **kwargs):
763851 logger .info ('All done.' )
764852
765853 def sign_windows (self , file , identity , src_dir ):
766- pass
854+ # Check for signtool
855+ if not _cmd_exists ('signtool.exe' ):
856+ raise Error ('signtool was not found on the PATH.' )
857+
858+ signtool_args = ['signtool' , 'sign' , '/fd' , 'sha256' , '/tr' , 'http://timestamp.digicert.com' , '/td' , 'sha256' ]
859+ if not identity :
860+ signtool_args += ['/a' ]
861+ else :
862+ signtool_args += ['/sha1' , identity ]
863+ signtool_args += ['/d' , file .name , str (file .resolve ())]
864+
865+ _run (signtool_args , cwd = src_dir , capture_output = False , shell = True )
767866
768867 # noinspection PyMethodMayBeStatic
769868 def _macos_validate_keychain_profile (self , keychain_profile ):
0 commit comments