Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 30 additions & 65 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"'
Comment thread
jh-block marked this conversation as resolved.

# Build for Intel Mac
release-intel:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion crates/goose-cli/src/commands/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions ui/desktop/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 127 additions & 7 deletions ui/desktop/scripts/prepare-platform-binaries.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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...`);
Expand Down Expand Up @@ -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...');

Expand All @@ -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;
}

Expand All @@ -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);
Expand All @@ -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 };
module.exports = { preparePlatformBinaries };
12 changes: 6 additions & 6 deletions ui/desktop/src/platform/windows/bin/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed ui/desktop/src/platform/windows/bin/uv.exe
Binary file not shown.
Binary file removed ui/desktop/src/platform/windows/bin/uvx.exe
Binary file not shown.
Loading