diff --git a/Justfile b/Justfile index bb4996370c..4c7860e547 100644 --- a/Justfile +++ b/Justfile @@ -28,48 +28,15 @@ release-binary: @echo "Generating OpenAPI schema..." cargo run -p goose-server --bin generate_schema -# release-windows docker build command -win_docker_build_sh := '''rustup target add x86_64-pc-windows-gnu && \ - apt-get update && \ - apt-get install -y mingw-w64 protobuf-compiler cmake && \ - export CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc && \ - export CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++ && \ - export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar && \ - export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc && \ - export PKG_CONFIG_ALLOW_CROSS=1 && \ - export PROTOC=/usr/bin/protoc && \ - export PATH=/usr/bin:\$PATH && \ - protoc --version && \ - cargo build --release --target x86_64-pc-windows-gnu && \ - GCC_DIR=\$(ls -d /usr/lib/gcc/x86_64-w64-mingw32/*/ | head -n 1) && \ - cp \$GCC_DIR/libstdc++-6.dll /usr/src/myapp/target/x86_64-pc-windows-gnu/release/ && \ - cp \$GCC_DIR/libgcc_s_seh-1.dll /usr/src/myapp/target/x86_64-pc-windows-gnu/release/ && \ - cp /usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll /usr/src/myapp/target/x86_64-pc-windows-gnu/release/ -''' - -# Build Windows executable +# Build Windows executable on a Windows host +[unix] release-windows: - #!/usr/bin/env sh - if [ "$(uname)" = "Darwin" ] || [ "$(uname)" = "Linux" ]; then - echo "Building Windows executable using Docker..." - docker volume create goose-windows-cache || true - docker run --rm \ - -v "$(pwd)":/usr/src/myapp \ - -v goose-windows-cache:/usr/local/cargo/registry \ - -w /usr/src/myapp \ - rust:latest \ - sh -c "{{win_docker_build_sh}}" - else - echo "Building Windows executable using Docker through PowerShell..." - powershell.exe -Command "docker volume create goose-windows-cache; \` - docker run --rm \` - -v ${PWD}:/usr/src/myapp \` - -v goose-windows-cache:/usr/local/cargo/registry \` - -w /usr/src/myapp \` - rust:latest \` - sh -c '{{win_docker_build_sh}}'" - fi - echo "Windows executable and required DLLs created at ./target/x86_64-pc-windows-gnu/release/" + @echo "just release-windows requires a Windows host because Goose Windows releases build the MSVC target. Use .github/workflows/bundle-desktop-windows.yml for CI builds." + @exit 1 + +[windows] +release-windows: + @powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'rustup target add x86_64-pc-windows-msvc; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; cargo build --release --target x86_64-pc-windows-msvc -p goose-server; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; Write-Host "Windows executable created at ./target/x86_64-pc-windows-msvc/release/goosed.exe"' # Build for Intel Mac release-intel: @@ -110,16 +77,22 @@ copy-binary-intel: exit 1; \ fi -# Copy Windows binary command +# Copy Windows binary command on a Windows host +[unix] copy-binary-windows: - @powershell.exe -Command "if (Test-Path ./target/x86_64-pc-windows-gnu/release/goosed.exe) { \ - Write-Host 'Copying Windows binary and DLLs to ui/desktop/src/bin...'; \ - Copy-Item -Path './target/x86_64-pc-windows-gnu/release/goosed.exe' -Destination './ui/desktop/src/bin/' -Force; \ - Copy-Item -Path './target/x86_64-pc-windows-gnu/release/*.dll' -Destination './ui/desktop/src/bin/' -Force; \ + @echo "just copy-binary-windows requires a Windows host because it copies the MSVC build output." + @exit 1 + +[windows] +copy-binary-windows: + @powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'if (Test-Path ./target/x86_64-pc-windows-msvc/release/goosed.exe) { \ + Write-Host "Copying Windows binary to ui/desktop/src/bin..."; \ + New-Item -ItemType Directory -Force "./ui/desktop/src/bin" | Out-Null; \ + Copy-Item -Path "./target/x86_64-pc-windows-msvc/release/goosed.exe" -Destination "./ui/desktop/src/bin/" -Force; \ } else { \ - Write-Host 'Windows binary not found.' -ForegroundColor Red; \ + Write-Host "Windows binary not found." -ForegroundColor Red; \ exit 1; \ - }" + }' # Run UI with latest run-ui: @@ -247,25 +220,17 @@ make-ui: @just release-binary cd ui/desktop && pnpm run bundle:default -# make GUI with latest Windows binary +# make GUI with latest Windows binary on a Windows host +[unix] +make-ui-windows: + @echo "just make-ui-windows requires a Windows host because Goose Windows releases build the MSVC target. Use .github/workflows/bundle-desktop-windows.yml for CI builds." + @exit 1 + +[windows] make-ui-windows: @just release-windows - #!/usr/bin/env sh - set -e - if [ -f "./target/x86_64-pc-windows-gnu/release/goosed.exe" ]; then \ - echo "Cleaning destination directory..." && \ - rm -rf ./ui/desktop/src/bin && \ - mkdir -p ./ui/desktop/src/bin && \ - echo "Copying Windows binary and DLLs..." && \ - cp -f ./target/x86_64-pc-windows-gnu/release/goosed.exe ./ui/desktop/src/bin/ && \ - cp -f ./target/x86_64-pc-windows-gnu/release/*.dll ./ui/desktop/src/bin/ && \ - echo "Starting Windows package build..." && \ - (cd ui/desktop && pnpm run bundle:windows) && \ - echo "Windows package build complete!"; \ - else \ - echo "Windows binary not found."; \ - exit 1; \ - fi + @just copy-binary-windows + @powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'Set-Location ui/desktop; $env:ELECTRON_PLATFORM="win32"; node scripts/prepare-platform-binaries.js; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; pnpm run make --platform=win32 --arch=x64; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; Write-Host "Windows package build complete!"' # make GUI with latest binary make-ui-intel: diff --git a/crates/goose-cli/src/commands/update.rs b/crates/goose-cli/src/commands/update.rs index 732fc64648..73a69503b0 100644 --- a/crates/goose-cli/src/commands/update.rs +++ b/crates/goose-cli/src/commands/update.rs @@ -513,7 +513,6 @@ fn replace_binary(new_binary: &Path, current_exe: &Path) -> Result<()> { // --------------------------------------------------------------------------- /// Copy any .dll files from the extracted archive alongside the installed binary. -/// Windows GNU builds ship with libgcc, libstdc++, libwinpthread DLLs. #[cfg(target_os = "windows")] fn copy_dlls(extracted_binary: &Path, current_exe: &Path) -> Result<()> { let source_dir = extracted_binary diff --git a/ui/desktop/.gitignore b/ui/desktop/.gitignore index 363303194a..18cebd5e1b 100644 --- a/ui/desktop/.gitignore +++ b/ui/desktop/.gitignore @@ -5,6 +5,8 @@ src/bin/goosed src/bin/goose-npm/ /src/bin/*.exe /src/bin/*.cmd +/src/platform/windows/bin/*.dll +/src/platform/windows/bin/*.exe /playwright-report/ /test-results/ /src/bin/temporal-service diff --git a/ui/desktop/scripts/prepare-platform-binaries.js b/ui/desktop/scripts/prepare-platform-binaries.js index 300827d23a..9679d908b7 100644 --- a/ui/desktop/scripts/prepare-platform-binaries.js +++ b/ui/desktop/scripts/prepare-platform-binaries.js @@ -1,9 +1,19 @@ const fs = require('fs'); +const crypto = require('crypto'); +const https = require('https'); +const os = require('os'); const path = require('path'); +const { execFileSync } = require('child_process'); // Paths const srcBinDir = path.join(__dirname, '..', 'src', 'bin'); const platformWinDir = path.join(__dirname, '..', 'src', 'platform', 'windows', 'bin'); +const uvVersion = '0.11.11'; +const uvDownloadUrl = `https://github.com/astral-sh/uv/releases/download/${uvVersion}/uv-x86_64-pc-windows-msvc.zip`; +const uvBinaryHashes = { + 'uv.exe': 'b1645e948603c12dd741987d0c072471195e18dd299b42334477ceac694f0af8', + 'uvx.exe': '0305c488dc29c16df1483c02a902d21a6798b0744f8e9eb34271d6b3e4bf6e2a', +}; // Platform-specific file patterns const windowsFiles = [ @@ -49,6 +59,106 @@ function matchesPattern(filename, patterns) { }); } +function sha256(filePath) { + const hash = crypto.createHash('sha256'); + hash.update(fs.readFileSync(filePath)); + return hash.digest('hex'); +} + +function hasExpectedHash(filePath, expectedHash) { + return fs.existsSync(filePath) && sha256(filePath) === expectedHash; +} + +function downloadFile(url, destPath, redirectsRemaining = 5) { + return new Promise((resolve, reject) => { + https.get(url, response => { + if ( + response.statusCode >= 300 && + response.statusCode < 400 && + response.headers.location && + redirectsRemaining > 0 + ) { + response.resume(); + downloadFile(response.headers.location, destPath, redirectsRemaining - 1) + .then(resolve) + .catch(reject); + return; + } + + if (response.statusCode !== 200) { + response.resume(); + reject(new Error(`Failed to download ${url}: HTTP ${response.statusCode}`)); + return; + } + + const file = fs.createWriteStream(destPath); + response.pipe(file); + file.on('finish', () => file.close(resolve)); + file.on('error', reject); + }).on('error', reject); + }); +} + +function extractZip(zipPath, destDir) { + if (process.platform === 'win32') { + execFileSync( + 'powershell.exe', + [ + '-NoProfile', + '-ExecutionPolicy', + 'Bypass', + '-Command', + `Expand-Archive -LiteralPath '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destDir.replace(/'/g, "''")}' -Force`, + ], + { stdio: 'inherit' } + ); + return; + } + + execFileSync('unzip', ['-q', zipPath, '-d', destDir], { stdio: 'inherit' }); +} + +async function ensureWindowsUvBinaries() { + const allPresent = Object.entries(uvBinaryHashes).every(([name, expectedHash]) => + hasExpectedHash(path.join(srcBinDir, name), expectedHash) + ); + + if (allPresent) { + console.log(`Pinned uv ${uvVersion} binaries already present`); + return; + } + + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'goose-uv-')); + const zipPath = path.join(tmpDir, 'uv.zip'); + const extractDir = path.join(tmpDir, 'extract'); + fs.mkdirSync(extractDir, { recursive: true }); + + try { + console.log(`Downloading uv ${uvVersion} from ${uvDownloadUrl}`); + await downloadFile(uvDownloadUrl, zipPath); + extractZip(zipPath, extractDir); + + for (const [name, expectedHash] of Object.entries(uvBinaryHashes)) { + const extractedPath = path.join(extractDir, name); + if (!fs.existsSync(extractedPath)) { + throw new Error(`Downloaded uv archive did not contain ${name}`); + } + + const actualHash = sha256(extractedPath); + if (actualHash !== expectedHash) { + throw new Error( + `${name} checksum mismatch for uv ${uvVersion}: expected ${expectedHash}, got ${actualHash}` + ); + } + + fs.copyFileSync(extractedPath, path.join(srcBinDir, name)); + console.log(`Copied pinned ${name}`); + } + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +} + // Helper function to clean directory of cross-platform files function cleanBinDirectory(targetPlatform) { console.log(`Cleaning bin directory for ${targetPlatform} build...`); @@ -95,7 +205,7 @@ function cleanBinDirectory(targetPlatform) { } // Helper function to copy platform-specific files -function copyPlatformFiles(targetPlatform) { +async function copyPlatformFiles(targetPlatform) { if (targetPlatform === 'win32') { console.log('Copying Windows-specific files...'); @@ -109,10 +219,15 @@ function copyPlatformFiles(targetPlatform) { fs.mkdirSync(srcBinDir, { recursive: true }); } - // Copy Windows-specific files + // Copy Windows-specific scripts and authored support files. const files = fs.readdirSync(platformWinDir, { withFileTypes: true }); files.forEach(file => { - if (file.name === 'README.md' || file.name === '.gitignore') { + if ( + file.name === 'README.md' || + file.name === '.gitignore' || + file.name.endsWith('.exe') || + file.name.endsWith('.dll') + ) { return; } @@ -127,17 +242,19 @@ function copyPlatformFiles(targetPlatform) { console.log(`Copied: ${file.name}`); } }); + + await ensureWindowsUvBinaries(); } } // Main function -function preparePlatformBinaries() { +async function preparePlatformBinaries() { const targetPlatform = process.env.ELECTRON_PLATFORM || process.platform; console.log(`Preparing binaries for platform: ${targetPlatform}`); // First copy platform-specific files if needed - copyPlatformFiles(targetPlatform); + await copyPlatformFiles(targetPlatform); // Then clean up cross-platform files cleanBinDirectory(targetPlatform); @@ -147,7 +264,10 @@ function preparePlatformBinaries() { // Run if called directly if (require.main === module) { - preparePlatformBinaries(); + preparePlatformBinaries().catch(error => { + console.error(error); + process.exit(1); + }); } -module.exports = { preparePlatformBinaries }; \ No newline at end of file +module.exports = { preparePlatformBinaries }; diff --git a/ui/desktop/src/platform/windows/bin/README.md b/ui/desktop/src/platform/windows/bin/README.md index c385bba42a..75b1b95d86 100644 --- a/ui/desktop/src/platform/windows/bin/README.md +++ b/ui/desktop/src/platform/windows/bin/README.md @@ -1,6 +1,6 @@ -# Windows-Specific Binaries +# Windows-Specific Runtime Files -This directory contains Windows-specific binaries and scripts that are only included during Windows builds. +This directory contains Windows-specific scripts that are only included during Windows builds. ## Components @@ -9,13 +9,13 @@ This directory contains Windows-specific binaries and scripts that are only incl - `npx.cmd` - Wrapper script that ensures Node.js is installed and uses system npx ### Windows Binaries -- `*.dll` files - Required Windows dynamic libraries -- `*.exe` files - Windows executables +- `uv.exe` and `uvx.exe` are downloaded from the pinned Astral uv release during packaging. +- Compiled `.exe` and `.dll` files are generated or fetched during the build and are not committed. ## Build Process -These files are generated during the Windows build process by: +Windows runtime files are prepared during the build process by: 1. `prepare-windows-npm.sh` - Creates Node.js installation scripts -2. `copy-windows-dlls.js` - Copies all Windows-specific files to the output directory +2. `prepare-platform-binaries.js` - Downloads pinned uv binaries and copies Windows-specific files to `src/bin` None of these files should be committed to the repository - they are generated fresh during each Windows build. diff --git a/ui/desktop/src/platform/windows/bin/libgcc_s_seh-1.dll b/ui/desktop/src/platform/windows/bin/libgcc_s_seh-1.dll deleted file mode 100755 index 2a858d4fc7..0000000000 Binary files a/ui/desktop/src/platform/windows/bin/libgcc_s_seh-1.dll and /dev/null differ diff --git a/ui/desktop/src/platform/windows/bin/libstdc++-6.dll b/ui/desktop/src/platform/windows/bin/libstdc++-6.dll deleted file mode 100755 index e6c135f72b..0000000000 Binary files a/ui/desktop/src/platform/windows/bin/libstdc++-6.dll and /dev/null differ diff --git a/ui/desktop/src/platform/windows/bin/libwinpthread-1.dll b/ui/desktop/src/platform/windows/bin/libwinpthread-1.dll deleted file mode 100755 index d87e531869..0000000000 Binary files a/ui/desktop/src/platform/windows/bin/libwinpthread-1.dll and /dev/null differ diff --git a/ui/desktop/src/platform/windows/bin/uv.exe b/ui/desktop/src/platform/windows/bin/uv.exe deleted file mode 100644 index dbfa26d83c..0000000000 Binary files a/ui/desktop/src/platform/windows/bin/uv.exe and /dev/null differ diff --git a/ui/desktop/src/platform/windows/bin/uvx.exe b/ui/desktop/src/platform/windows/bin/uvx.exe deleted file mode 100644 index 15b125d4f7..0000000000 Binary files a/ui/desktop/src/platform/windows/bin/uvx.exe and /dev/null differ