diff --git a/psxrecomp_cli.py b/psxrecomp_cli.py index c7a35fcd..1d94932e 100755 --- a/psxrecomp_cli.py +++ b/psxrecomp_cli.py @@ -1617,19 +1617,43 @@ def _pack_sysroot_cmake_args( return [f"-DCMAKE_SYSROOT={sysroot}", "-DOpenGL_GL_PREFERENCE=LEGACY"] -def _read_cmake_cache_generator(cache_file: Path) -> str: +def _read_cmake_cache_entry(cache_file: Path, key: str) -> str: if not cache_file.is_file(): return "" try: text = cache_file.read_text(encoding="utf-8", errors="replace") except OSError: return "" + prefix = f"{key}:" for line in text.splitlines(): - if line.startswith("CMAKE_GENERATOR:INTERNAL="): + if line.startswith(prefix) and "=" in line: return line.split("=", 1)[1].strip() return "" +def _read_cmake_cache_generator(cache_file: Path) -> str: + return _read_cmake_cache_entry(cache_file, "CMAKE_GENERATOR") + + +def _cmake_for_build_dir(build_dir: Path) -> Path: + """The cmake that configured *build_dir* (CMakeCache CMAKE_COMMAND). + + Building must use the same cmake as configure. A bare ``cmake`` resolves + through PATH, and on Windows that is often an MSYS2 cmake that rewrites + the Windows build path as ``/f/.../F:/...`` and fails. + """ + cached = _read_cmake_cache_entry(build_dir / "CMakeCache.txt", "CMAKE_COMMAND") + if cached and Path(cached).is_file(): + return Path(cached) + found = _which_tool("cmake") + if found is None: + raise RuntimeError( + f"cmake not found: {build_dir} has no configured CMAKE_COMMAND " + "and no cmake is on PATH" + ) + return found + + def _cmake_configure( project_root: Path, build_dir: Path, @@ -1759,7 +1783,7 @@ def _cmake_build( os.cpu_count() or 4 ) cmd = [ - "cmake", + str(_cmake_for_build_dir(build_dir)), "--build", str(build_dir), "--parallel", diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 7058f495..5e062cb9 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -953,8 +953,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/../tools/tests/test_cli_generate_aot_static.py WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) # psxrecomp_cli.py rebuild: the pre-build mtime clamp works without - # lutimes (Windows), and --cmake-extra wins over the shipping - # PSX_DEBUG_TOOLS=OFF default. + # lutimes (Windows), --cmake-extra wins over the shipping + # PSX_DEBUG_TOOLS=OFF default, and the build runs the configuring cmake. add_test( NAME cli_rebuild_host COMMAND ${Python3_EXECUTABLE} diff --git a/tools/new_project_layout/setup_project.ps1 b/tools/new_project_layout/setup_project.ps1 index 51db2ee6..053268d6 100644 --- a/tools/new_project_layout/setup_project.ps1 +++ b/tools/new_project_layout/setup_project.ps1 @@ -80,7 +80,41 @@ function Assert-Cmd([string]$cmd) { } } Assert-Cmd git -Assert-Cmd python + +# Windows-native Python only. An MSYS2/Cygwin python (e.g. devkitPro's, often +# first on PATH) ignores the Windows-style PYTHONPATH set below, so every +# sibling helper import fails. +function Resolve-Python { + if ($env:PSXRECOMP_PYTHON) { + if (Test-Path -LiteralPath $env:PSXRECOMP_PYTHON) { return $env:PSXRECOMP_PYTHON } + throw "PSXRECOMP_PYTHON does not exist: $env:PSXRECOMP_PYTHON" + } + if (Get-Command py -ErrorAction SilentlyContinue) { + $exe = (& py -3 -c "import sys; print(sys.executable)" 2>$null) + if ($LASTEXITCODE -eq 0 -and $exe) { return ([string]$exe).Trim() } + } + foreach ($c in @(Get-Command python -All -CommandType Application -ErrorAction SilentlyContinue)) { + if ($c.Source -notmatch '(?i)[\\/](msys64|msys2|msys|cygwin64|cygwin|devkitPro)[\\/]') { + return $c.Source + } + } + throw "No Windows-native Python 3 found (MSYS2/Cygwin pythons cannot import the scaffold helpers). Install Python from python.org or set PSXRECOMP_PYTHON." +} +$Python = Resolve-Python + +# Windows PowerShell 5.1 writes a BOM for -Encoding UTF8 and UTF-16LE for +# Tee-Object/Out-File. Block files are spliced mid-CMakeLists.txt, where a BOM +# is a CMake parse error, so every file this script writes goes through here. +$Utf8NoBom = New-Object System.Text.UTF8Encoding($false) +function Resolve-OutPath([string]$Path) { + return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) +} +function Write-TextFile([string]$Path, [string]$Value) { + [System.IO.File]::WriteAllText((Resolve-OutPath $Path), $Value + [Environment]::NewLine, $Utf8NoBom) +} +function Add-TextFile([string]$Path, [string]$Value) { + [System.IO.File]::AppendAllText((Resolve-OutPath $Path), $Value + [Environment]::NewLine, $Utf8NoBom) +} function Test-Interactive { return [Environment]::UserInteractive -and -not $Yes -and -not ($env:PSXRECOMP_SETUP_YES -eq "1") @@ -136,7 +170,7 @@ if ($Players -lt 1 -or $Players -gt 8) { } $env:PYTHONPATH = "$ScriptDir" + $(if ($env:PYTHONPATH) { ";$env:PYTHONPATH" } else { "" }) -$derivedZip = (python -c "from fill_tokens import derive_zip_prefix; import sys; print(derive_zip_prefix(sys.argv[1]))" $Name).Trim() +$derivedZip = (& $Python -c "from fill_tokens import derive_zip_prefix; import sys; print(derive_zip_prefix(sys.argv[1]))" $Name).Trim() if (-not $ZipPrefix) { if ($interactive) { $ZipPrefix = Prompt-Line "Zip / CI artifact prefix" $derivedZip @@ -152,7 +186,7 @@ if (-not $GithubOwner) { $GithubOwner = "RetroPortingToolKit" } } -$derivedRepo = (python -c "from fill_tokens import sanitize_github_name; import sys; print(sanitize_github_name(sys.argv[1]))" $Name).Trim() +$derivedRepo = (& $Python -c "from fill_tokens import sanitize_github_name; import sys; print(sanitize_github_name(sys.argv[1]))" $Name).Trim() if (-not $GithubRepo) { if ($interactive) { $GithubRepo = Prompt-Line "GitHub repo name (README download badges)" $derivedRepo @@ -160,8 +194,8 @@ if (-not $GithubRepo) { $GithubRepo = $derivedRepo } } -$GithubOwner = (python -c "from fill_tokens import sanitize_github_name; import sys; print(sanitize_github_name(sys.argv[1]))" $GithubOwner).Trim() -$GithubRepo = (python -c "from fill_tokens import sanitize_github_name; import sys; print(sanitize_github_name(sys.argv[1]))" $GithubRepo).Trim() +$GithubOwner = (& $Python -c "from fill_tokens import sanitize_github_name; import sys; print(sanitize_github_name(sys.argv[1]))" $GithubOwner).Trim() +$GithubRepo = (& $Python -c "from fill_tokens import sanitize_github_name; import sys; print(sanitize_github_name(sys.argv[1]))" $GithubRepo).Trim() if (-not $PSBoundParameters.ContainsKey("Description")) { if ($interactive) { $Description = Prompt-Line "Short game description (optional)" "" } @@ -282,20 +316,20 @@ $RecompUiBlockFile = Join-Path $env:TEMP "psxrecomp_recomp_ui_block.cmake" $CodegenArgFile = Join-Path $env:TEMP "psxrecomp_codegen_arg.cmake" if ($useRecompUi) { - Set-Content -Encoding UTF8 -Path $RecompUiBlockFile -Value "# recomp-ui submodule present (PSX_RECOMP_UI defaults ON)." - Set-Content -Encoding UTF8 -Path $CodegenArgFile -Value ' CODEGEN_SETUP_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/codegen_setup.c"' + Write-TextFile $RecompUiBlockFile "# recomp-ui submodule present (PSX_RECOMP_UI defaults ON)." + Write-TextFile $CodegenArgFile ' CODEGEN_SETUP_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/codegen_setup.c"' } else { - Set-Content -Encoding UTF8 -Path $RecompUiBlockFile -Value @" + Write-TextFile $RecompUiBlockFile @" set(PSX_RECOMP_UI OFF CACHE BOOL "No recomp-ui submodule in this scaffold" FORCE) "@ - Set-Content -Encoding UTF8 -Path $CodegenArgFile -Value " # CODEGEN_SETUP_SOURCES -- add with recomp-ui / wizard later" + Write-TextFile $CodegenArgFile " # CODEGEN_SETUP_SOURCES -- add with recomp-ui / wizard later" } if ($useNetplay) { # PSX_NETPLAY defaults RNET_ENABLE_ICE=ON; recomp-net FetchContents # libjuice via pinned URL (not git) so Retro AppImage builds configure. - Set-Content -Encoding UTF8 -Path $NetplayBlockFile -Value @" + Write-TextFile $NetplayBlockFile @" if(EXISTS "`${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt") set(PSX_NETPLAY ON CACHE BOOL "Link recomp-net delay-sync (opt-in; needs recomp-net)" FORCE) @@ -304,7 +338,7 @@ endif() $NetplayRuntimeArg = " ENABLE_NETPLAY_IF_PRESENT" $NetplayLobbyUrlArg = " NETPLAY_LOBBY_URL `"$NetplayLobbyWs`"" } else { - Set-Content -Encoding UTF8 -Path $NetplayBlockFile -Value @" + Write-TextFile $NetplayBlockFile @" # Full netplay UI -- enable after adding recomp-ui + testing: # if(EXISTS "`${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt") # set(PSX_NETPLAY ON CACHE BOOL "..." FORCE) @@ -315,26 +349,26 @@ endif() } if ($useWizard) { - Set-Content -Encoding UTF8 -Path $WizardBlockFile -Value @" + Write-TextFile $WizardBlockFile @" set(PSX_SETUP_WIZARD ON CACHE BOOL "Advertise first-run setup wizard + Generate & rebuild in recomp-ui" FORCE) "@ $WizardRuntimeArg = " ENABLE_SETUP_WIZARD" } else { - Set-Content -Encoding UTF8 -Path $WizardBlockFile -Value @" + Write-TextFile $WizardBlockFile @" # First-run setup wizard -- enable after testing: # set(PSX_SETUP_WIZARD ON CACHE BOOL "..." FORCE) "@ $WizardRuntimeArg = " # ENABLE_SETUP_WIZARD" } -Set-Content -Encoding UTF8 -Path $BoxartBlockFile -Value ' # LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"' +Write-TextFile $BoxartBlockFile ' # LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"' $HasBoxart = $false $AppIconBlockFile = Join-Path $env:TEMP "psxrecomp_app_icon_block.cmake" -Set-Content -Encoding UTF8 -Path $AppIconBlockFile -Value ' APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico"' +Write-TextFile $AppIconBlockFile ' APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico"' function Fill-Template([string]$src, [string]$dst) { - python $FillTokens $src $dst ` + & $Python $FillTokens $src $dst ` --set "PROJECT_CMAKE_NAME=$ProjectCmakeName" ` --set "WINDOW_TITLE=$WindowTitle" ` --set "BOOT_EXE=$BootExe" ` @@ -400,7 +434,7 @@ New-Item -ItemType Directory -Force -Path (Join-Path $Root "tools") | Out-Null New-Item -ItemType Directory -Force -Path (Join-Path $Root "mods\preloaded\packages") | Out-Null New-Item -ItemType Directory -Force -Path (Join-Path $Root "assets") | Out-Null # Empty mod catalog tree (build stages mods/preloaded/packages -> /mods/bundled). -@' +$PreloadedReadme = @' # Preloaded mods Ship reviewed, default-disabled packages here: @@ -420,7 +454,8 @@ launcher owns and no build ever touches. Install them through the launcher Mods manager rather than committing them here. See `psxrecomp/docs/MOD_PACKAGES.md`. -'@ | Set-Content -Encoding utf8 (Join-Path $Root "mods\preloaded\README.md") +'@ +Write-TextFile (Join-Path $Root "mods\preloaded\README.md") $PreloadedReadme New-Item -ItemType File -Force -Path (Join-Path $Root "mods\preloaded\packages\.gitkeep") | Out-Null Copy-Item (Join-Path $ScriptDir "sync_symbols.py") (Join-Path $Root "tools\sync_symbols.py") @@ -460,7 +495,9 @@ if (Test-Path "psxrecomp\lib\recomp-net") { Write-Host "== Framework pins ==" $PinsPath = Join-Path $Root "framework_pins.txt" if (Test-Path "psxrecomp\tools\ci\record_pins.sh") { - bash psxrecomp/tools/ci/record_pins.sh --root $Root | Tee-Object -FilePath $PinsPath + $pinLines = @(bash psxrecomp/tools/ci/record_pins.sh --root $Root) + Write-TextFile $PinsPath ($pinLines -join [Environment]::NewLine) + $pinLines } else { $lines = @("psxrecomp=$(git -C psxrecomp rev-parse HEAD)") if ($useRecompUi) { @@ -468,7 +505,8 @@ if (Test-Path "psxrecomp\tools\ci\record_pins.sh") { } else { $lines += "recomp-ui=" } - $lines | Tee-Object -FilePath $PinsPath + Write-TextFile $PinsPath ($lines -join [Environment]::NewLine) + $lines } if ($useNetplay -and -not (Test-Path "psxrecomp\lib\recomp-net\CMakeLists.txt")) { @@ -484,7 +522,7 @@ if ($useCi) { $WfDst = Join-Path $Root ".github\workflows\release.yml" if (Test-Path -LiteralPath $WfSrc) { New-Item -ItemType Directory -Force -Path (Split-Path -Parent $WfDst) | Out-Null - python $FillTokens $WfSrc $WfDst ` + & $Python $FillTokens $WfSrc $WfDst ` --ci-placeholders ` --set "ZIP_PREFIX=$ZipPrefix" ` --set "GAME_TITLE=$WindowTitle" ` @@ -532,10 +570,11 @@ for m in re.finditer(r'FILE\s+"([^"]+)"', text, re.I): print(" disc staged under disc/ (gitignored -- never commit dumps)") '@ $pyFile = Join-Path $env:TEMP "psxrecomp_stage_disc.py" - Set-Content -Encoding UTF8 -Path $pyFile -Value $py - python $pyFile $DiscAbs $DiscOut + Write-TextFile $pyFile $py + & $Python $pyFile $DiscAbs $DiscOut $ProbeCue = Join-Path $DiscOut $DiscBasename $DiscTomlPath = "disc/$DiscBasename" + $GenDiscHint = $ProbeCue } else { Write-Host "== External disc (no full copy) ==" Write-Host " cue remains at: $DiscAbs" @@ -547,7 +586,7 @@ $SeedCount = 0 if ($DiscBasename -match '\.cue$') { Write-Host "== Probing disc (identity + seeds + TOC fp) ==" try { - python $ProbeDisc $ProbeCue ` + & $Python $ProbeDisc $ProbeCue ` --json-out (Join-Path $Root "disc_probe.json") ` --write-game-toml (Join-Path $Root "game.toml") ` --write-catalog (Join-Path $Root "catalog_identity.json") ` @@ -563,14 +602,14 @@ if ($DiscBasename -match '\.cue$') { --region $Region $Probed = $true $SeedCount = (Select-String -Path (Join-Path $Root "seeds\ghidra_funcs.txt") -Pattern '^0x').Count - $BootFromToml = python -c @" + $BootFromToml = & $Python -c @" import re,sys t=open(sys.argv[1],encoding='utf-8').read() m=re.search(r'boot_exe\s*=\s*\"([^\"]+)\"', t) print(m.group(1) if m else '') "@ (Join-Path $Root "game.toml") $BootFromToml = $BootFromToml.Trim() - $EntryFromToml = python -c @" + $EntryFromToml = & $Python -c @" import re,sys t=open(sys.argv[1],encoding='utf-8').read() m=re.search(r'entry_pc\s*=\s*\"([^\"]+)\"', t) @@ -599,9 +638,9 @@ print(m.group(1) if m else '') Write-Warning "-Disc is not a .cue; skipped probe autofill." } -# Community metadata (https://recomp.fyi/spec) — serial from the probe if it ran. +# Community metadata (https://recomp.fyi/spec) -- serial from the probe if it ran. Write-Host "== Writing .recomp.json ==" -python $WriteRecompJson (Join-Path $Root ".recomp.json") ` +& $Python $WriteRecompJson (Join-Path $Root ".recomp.json") ` --game $GameName ` --project $Name ` --region $Region ` @@ -613,23 +652,23 @@ if ($doBoxart) { $cueHint = if ($DiscBasename) { $DiscBasename } else { $GameName } $prevPy = $env:PYTHONPATH try { - python $FetchBoxartPy ` + & $Python $FetchBoxartPy ` --out (Join-Path $Root "launcher_assets\img\boxart.tga") ` --cue-stem $cueHint ` --display-name $GameName if ($LASTEXITCODE -eq 0) { $HasBoxart = $true - Set-Content -Encoding UTF8 -Path $BoxartBlockFile -Value ' LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"' + Write-TextFile $BoxartBlockFile ' LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"' Fill-Template (Join-Path $TemplateDir "CMakeLists.txt.in") (Join-Path $Root "CMakeLists.txt") Write-Host " wired LAUNCHER_BOXART in CMakeLists.txt" $env:PYTHONPATH = $ScriptDir - python -c "from pathlib import Path; import sys; from project_studio.readme_metrics import inject_readme_boxart; inject_readme_boxart(Path(sys.argv[1]), sys.argv[2])" (Join-Path $Root "README.md") $GameName + & $Python -c "from pathlib import Path; import sys; from project_studio.readme_metrics import inject_readme_boxart; inject_readme_boxart(Path(sys.argv[1]), sys.argv[2])" (Join-Path $Root "README.md") $GameName Write-Host " injected boxart into README.md" } else { - Write-Warning "boxart fetch failed — leave LAUNCHER_BOXART commented." + Write-Warning "boxart fetch failed -- leave LAUNCHER_BOXART commented." } } catch { - Write-Warning "boxart fetch failed — leave LAUNCHER_BOXART commented." + Write-Warning "boxart fetch failed -- leave LAUNCHER_BOXART commented." } finally { if ($null -eq $prevPy) { Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue } else { $env:PYTHONPATH = $prevPy } } @@ -637,9 +676,28 @@ if ($doBoxart) { Write-Host "== Sync symbols header ==" Push-Location $Root -try { python tools/sync_symbols.py --game $GameName } catch { Write-Warning "sync_symbols failed" } +try { & $Python tools/sync_symbols.py --game $GameName } catch { Write-Warning "sync_symbols failed" } Pop-Location +# Every emitted text file must be BOM-less UTF-8: a U+FEFF spliced into +# CMakeLists.txt is a configure-time parse error, and UTF-16 pins break tools. +$TextExt = @(".txt", ".toml", ".json", ".md", ".c", ".h", ".cmake", ".sh", ".yml", ".yaml", ".py", ".ps1", ".gitignore") +$Bad = @() +Get-ChildItem -LiteralPath $Root -Recurse -File -Force | + Where-Object { $_.FullName -notmatch '[\\/](psxrecomp|recomp-ui|disc|\.git)[\\/]' -and ($TextExt -contains $_.Extension.ToLower() -or $_.Name -eq "VERSION") } | + ForEach-Object { + $bytes = [System.IO.File]::ReadAllBytes($_.FullName) + $utf16 = $bytes.Length -ge 2 -and (($bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) -or ($bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF)) + $bom = $false + for ($i = 0; $i + 2 -lt $bytes.Length; $i++) { + if ($bytes[$i] -eq 0xEF -and $bytes[$i + 1] -eq 0xBB -and $bytes[$i + 2] -eq 0xBF) { $bom = $true; break } + } + if ($utf16 -or $bom) { $Bad += $_.FullName.Substring($Root.Length + 1) } + } +if ($Bad.Count -gt 0) { + throw "Scaffold emitted BOM/UTF-16 text (must be BOM-less UTF-8): $($Bad -join ', ')" +} + git add CMakeLists.txt game.toml codegen_setup.c codegen_setup.h .gitignore VERSION README.md seeds scripts tools mods framework_pins.txt symbols.toml psx_symbols.h 2>$null if (Test-Path (Join-Path $Root ".github")) { git add .github 2>$null @@ -680,8 +738,8 @@ if ($createGithub) { default { "--private" } } $ghSlug = "$GithubOwner/$GithubRepo" - $ghDesc = (python -c "from fill_tokens import GITHUB_ABOUT_DESCRIPTION; print(GITHUB_ABOUT_DESCRIPTION)").Trim() - $ghHome = (python -c "from fill_tokens import GITHUB_ABOUT_HOMEPAGE; print(GITHUB_ABOUT_HOMEPAGE)").Trim() + $ghDesc = (& $Python -c "from fill_tokens import GITHUB_ABOUT_DESCRIPTION; print(GITHUB_ABOUT_DESCRIPTION)").Trim() + $ghHome = (& $Python -c "from fill_tokens import GITHUB_ABOUT_HOMEPAGE; print(GITHUB_ABOUT_HOMEPAGE)").Trim() try { gh repo create $ghSlug $vis --source=$Root --remote=origin --description "$ghDesc" --homepage "$ghHome" Write-Host " created origin ($GithubVisibility); push deferred until end" @@ -706,7 +764,7 @@ if ($createGithub) { gh repo edit $ghSlug --description "$ghDesc" --homepage "$ghHome" Write-Host " GitHub About: R.A.I.D. description + Discord homepage" } catch { - Write-Warning "gh repo edit (About) failed — set description/homepage by hand." + Write-Warning "gh repo edit (About) failed -- set description/homepage by hand." } } } @@ -729,7 +787,8 @@ if ($doGenerate) { $BiosAbs = (Resolve-Path -LiteralPath $Bios).Path $genArgs += @("--bios", $BiosAbs) } - python @genArgs + & $Python @genArgs + if ($LASTEXITCODE -ne 0) { throw "psxrecomp_cli generate failed (exit $LASTEXITCODE)" } $GeneratedOk = $true # Release CI builds the compiled game from this tree, so it is part # of the repo, not a local by-product. @@ -752,8 +811,8 @@ if ($doBuild -and $GeneratedOk) { Write-Host "== Configure & build psx-runtime ==" Push-Location $Root try { - cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release - cmake --build build-release --target psx-runtime + & $Python psxrecomp\psxrecomp_cli.py rebuild --config game.toml --project-root . --build-dir build-release --no-pgo + if ($LASTEXITCODE -ne 0) { throw "psxrecomp_cli rebuild failed (exit $LASTEXITCODE)" } $BuildOk = $true Write-Host " build OK -> build-release/ (gitignored)" } catch { @@ -794,7 +853,7 @@ function Ensure-ActionsRegistersReleaseYml([string]$OwnerRepo) { Write-Host " Actions has not listed release.yml yet -- nudging with a follow-up commit..." $stamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mmZ") - Add-Content -Encoding utf8 -Path $wfPath -Value "`n# Actions registration nudge ($stamp UTC)" + Add-TextFile $wfPath "`n# Actions registration nudge ($stamp UTC)" git add $wfPath 2>$null try { git -c user.email=setup@localhost -c user.name=setup ` @@ -848,8 +907,7 @@ $Step2 = if ($BuildOk) { @" Build playable runtime: - cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release - cmake --build build-release --target psx-runtime + python psxrecomp\psxrecomp_cli.py rebuild --config game.toml --project-root . --build-dir build-release --no-pgo "@ } else { @" @@ -858,8 +916,7 @@ Build emitters, Generate (and commit it -- release CI builds from generated/), t bash psxrecomp/tools/ci/build_emitters.sh python psxrecomp\psxrecomp_cli.py generate --config game.toml --project-root . --disc $GenDiscHint git add generated; git commit -m "Add generated game C" - cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release - cmake --build build-release --target psx-runtime + python psxrecomp\psxrecomp_cli.py rebuild --config game.toml --project-root . --build-dir build-release --no-pgo "@ } $CiNote = if ($useCi) { diff --git a/tools/tests/test_cli_rebuild_host.py b/tools/tests/test_cli_rebuild_host.py index cf6e060d..197eadf6 100644 --- a/tools/tests/test_cli_rebuild_host.py +++ b/tools/tests/test_cli_rebuild_host.py @@ -114,5 +114,57 @@ class RebuildConfigureFlagsTest(unittest.TestCase): [('generate', 'ON'), ('use', 'OFF')]) +class CmakeBuildToolTest(unittest.TestCase): + """Build must run the cmake that configured the tree, never a PATH cmake. + + On Windows a PATH ``cmake`` is often MSYS2's, which rewrites the Windows + build directory as ``/f/.../F:/...`` and fails. + """ + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.build = Path(self.tmp.name) / 'build-release' + self.build.mkdir() + self.configured = Path(self.tmp.name) / 'toolchain' / 'bin' / 'cmake.exe' + self.configured.parent.mkdir(parents=True) + self.configured.write_text('') + + def tearDown(self): + self.tmp.cleanup() + + def build_cmd(self): + calls = [] + + def run(cmd, **_): + calls.append(cmd) + return mock.Mock(returncode=0, stdout='', stderr='') + + with mock.patch.object(cli.subprocess, 'run', side_effect=run): + cli._cmake_build(self.build, 'psx-runtime', mock.Mock()) + return calls[0] + + def test_uses_cmake_command_from_cache(self): + (self.build / 'CMakeCache.txt').write_text( + 'CMAKE_GENERATOR:INTERNAL=Ninja\n' + f'CMAKE_COMMAND:INTERNAL={self.configured}\n') + with mock.patch.object(cli, '_which_tool', return_value=Path('/msys/usr/bin/cmake')): + self.assertEqual(self.build_cmd()[0], str(self.configured)) + + def test_falls_back_to_path_without_cache(self): + with mock.patch.object(cli, '_which_tool', return_value=Path('/usr/bin/cmake')): + self.assertEqual(self.build_cmd()[0], str(Path('/usr/bin/cmake'))) + + def test_stale_cache_entry_falls_back_to_path(self): + (self.build / 'CMakeCache.txt').write_text( + f'CMAKE_COMMAND:INTERNAL={self.configured.parent / "gone.exe"}\n') + with mock.patch.object(cli, '_which_tool', return_value=Path('/usr/bin/cmake')): + self.assertEqual(self.build_cmd()[0], str(Path('/usr/bin/cmake'))) + + def test_no_cmake_anywhere_is_an_error(self): + with mock.patch.object(cli, '_which_tool', return_value=None): + with self.assertRaises(RuntimeError): + cli._cmake_build(self.build, 'psx-runtime', mock.Mock()) + + if __name__ == '__main__': unittest.main()