Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ src/vs/base/browser/ui/codicons/codicon/codicon.ttf
/out*/
/extensions/**/out/
build/node_modules
build/darwin/.dmgbuild
coverage/
test_data/
test-results/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,6 @@ jobs:
- script: |
set -e
# Needed for https://github.com/dmgbuild/dmgbuild/blob/main/src/dmgbuild/badge.py
python3 -m pip install pyobjc-framework-Quartz
DMG_OUT="$(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_dmg"
mkdir -p $DMG_OUT
node build/darwin/create-dmg.ts $(agent.builddirectory) $DMG_OUT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,6 @@ steps:
- script: |
set -e
# Needed for https://github.com/dmgbuild/dmgbuild/blob/main/src/dmgbuild/badge.py
python3 -m pip install pyobjc-framework-Quartz
DMG_OUT="$(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_dmg"
mkdir -p $DMG_OUT
node build/darwin/create-dmg.ts $(agent.builddirectory) $DMG_OUT
Expand Down
185 changes: 129 additions & 56 deletions build/darwin/create-dmg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,131 @@ import { spawn } from '@malept/cross-spawn-promise';
const root = path.dirname(path.dirname(import.meta.dirname));
const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8'));

interface DmgBuildSettings {
title: string;
icon?: string | null;
'badge-icon'?: string | null;
background?: string;
'background-color'?: string;
'icon-size'?: number;
'text-size'?: number;
format?: string;
window?: {
position?: { x: number; y: number };
size?: { width: number; height: number };
};
contents: Array<{
path: string;
x: number;
y: number;
type: 'file' | 'link';
name?: string;
}>;
const DMGBUILD_REPO = 'https://github.com/dmgbuild/dmgbuild.git';
const DMGBUILD_COMMIT = '75c8a6c7835c5b73dfd4510d92a8f357f93a5fbf';
const MIN_PYTHON_VERSION = [3, 10];

function getDmgBuildPath(): string {
return path.join(import.meta.dirname, '.dmgbuild');
}

function getVenvPath(): string {
return path.join(getDmgBuildPath(), 'venv');
}

function getPythonPath(): string {
return path.join(getVenvPath(), 'bin', 'python3');
}

function getDmgBuilderPath(): string {
return path.join(import.meta.dirname, '..', 'node_modules', 'dmg-builder');
async function checkPythonVersion(pythonBin: string): Promise<boolean> {
try {
const output = await spawn(pythonBin, ['--version']);
const match = output.match(/Python (\d+)\.(\d+)/);
if (match) {
const major = parseInt(match[1], 10);
const minor = parseInt(match[2], 10);
return major > MIN_PYTHON_VERSION[0] || (major === MIN_PYTHON_VERSION[0] && minor >= MIN_PYTHON_VERSION[1]);
}
} catch {
// not available
}
return false;
}

/**
* Finds a Python binary that meets the minimum version requirement.
* Tries well-known candidates first, and if none are suitable,
* installs Python 3.12 via Homebrew.
*/
async function findSuitablePython(): Promise<string> {
const candidates = [
'python3',
'python3.12',
'python3.11',
'python3.10',
// Homebrew paths (Apple Silicon)
'/opt/homebrew/opt/python@3.12/bin/python3',
'/opt/homebrew/opt/python@3.11/bin/python3',
'/opt/homebrew/opt/python@3.10/bin/python3',
// Homebrew paths (Intel)
'/usr/local/opt/python@3.12/bin/python3',
'/usr/local/opt/python@3.11/bin/python3',
'/usr/local/opt/python@3.10/bin/python3',
];

for (const candidate of candidates) {
if (await checkPythonVersion(candidate)) {
console.log(`Found suitable Python: ${candidate}`);
return candidate;
}
}

console.log(`No Python >= ${MIN_PYTHON_VERSION[0]}.${MIN_PYTHON_VERSION[1]} found, installing via Homebrew...`);
await spawn('brew', ['install', 'python@3.12'], {
stdio: 'inherit',
env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: '1', HOMEBREW_NO_INSTALL_CLEANUP: '1' }
});

// Use `brew --prefix` to reliably locate the installation
const brewPrefix = (await spawn('brew', ['--prefix', 'python@3.12'])).trim();
const brewBinDir = path.join(brewPrefix, 'bin');
console.log(`Homebrew Python prefix: ${brewPrefix}`);

// Try both python3 and python3.12 (keg-only formulae may only have the versioned name)
for (const name of ['python3', 'python3.12']) {
const fullPath = path.join(brewBinDir, name);
if (await checkPythonVersion(fullPath)) {
console.log(`Using Homebrew Python: ${fullPath}`);
return fullPath;
}
}

throw new Error(`Could not find Python >= ${MIN_PYTHON_VERSION[0]}.${MIN_PYTHON_VERSION[1]} even after Homebrew install at ${brewPrefix}.`);
}

function getDmgBuilderVendorPath(): string {
return path.join(getDmgBuilderPath(), 'vendor');
async function ensureDmgBuild(): Promise<void> {
const dmgBuildPath = getDmgBuildPath();
const venvPath = getVenvPath();
const markerFile = path.join(dmgBuildPath, '.installed');
if (fs.existsSync(markerFile)) {
console.log('dmgbuild already installed, skipping setup');
return;
}

console.log('Setting up dmgbuild from GitHub...');
if (fs.existsSync(dmgBuildPath)) {
fs.rmSync(dmgBuildPath, { recursive: true });
}

console.log(`Cloning dmgbuild from ${DMGBUILD_REPO} at ${DMGBUILD_COMMIT}...`);
await spawn('git', ['clone', DMGBUILD_REPO, dmgBuildPath], {
stdio: 'inherit'
});
await spawn('git', ['-C', dmgBuildPath, 'checkout', DMGBUILD_COMMIT], {
stdio: 'inherit'
});

const pythonBin = await findSuitablePython();
console.log('Creating Python virtual environment...');
await spawn(pythonBin, ['-m', 'venv', venvPath], {
stdio: 'inherit'
});

console.log('Installing dmgbuild and dependencies into venv...');
const pipPath = path.join(venvPath, 'bin', 'pip');
await spawn(pipPath, ['install', dmgBuildPath], {
stdio: 'inherit'
});

fs.writeFileSync(markerFile, `Installed at ${new Date().toISOString()}\nCommit: ${DMGBUILD_COMMIT}\n`);
console.log('dmgbuild setup complete');
}

async function runDmgBuild(settingsFile: string, volumeName: string, artifactPath: string): Promise<void> {
const vendorDir = getDmgBuilderVendorPath();
const scriptPath = path.join(vendorDir, 'run_dmgbuild.py');
await spawn('python3', [scriptPath, '-s', settingsFile, volumeName, artifactPath], {
cwd: vendorDir,
await ensureDmgBuild();

const pythonPath = getPythonPath();
await spawn(pythonPath, ['-m', 'dmgbuild', '-s', settingsFile, volumeName, artifactPath], {
stdio: 'inherit'
});
}
Expand Down Expand Up @@ -98,34 +188,17 @@ async function main(buildDir?: string, outDir?: string): Promise<void> {
fs.unlinkSync(artifactPath);
}

const settings: DmgBuildSettings = {
title,
'badge-icon': diskIconPath,
background: backgroundPath,
format: 'ULMO',
'text-size': 12,
window: {
position: { x: 100, y: 400 },
size: { width: 480, height: 352 }
},
contents: [
{
path: appPath,
x: 120,
y: 160,
type: 'file'
},
{
path: '/Applications',
x: 360,
y: 160,
type: 'link'
}
]
};

const settingsFile = path.join(outDir, '.dmg-settings.json');
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
// Copy and process the settings template for dmgbuild
const settingsTemplatePath = path.join(import.meta.dirname, 'dmg-settings.py.template');
const settingsFile = path.join(outDir, '.dmg-settings.py');
let settingsContent = fs.readFileSync(settingsTemplatePath, 'utf8');
settingsContent = settingsContent
.replace('{{VOLUME_NAME}}', JSON.stringify(title))
.replace('{{BADGE_ICON}}', JSON.stringify(diskIconPath))
.replace('{{BACKGROUND}}', JSON.stringify(backgroundPath))
.replace('{{APP_PATH}}', JSON.stringify(appPath))
.replace('{{APP_NAME}}', JSON.stringify(product.nameLong + '.app'));
fs.writeFileSync(settingsFile, settingsContent);

try {
await runDmgBuild(settingsFile, dmgName, artifactPath);
Expand Down
38 changes: 38 additions & 0 deletions build/darwin/dmg-settings.py.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# dmgbuild settings template
# Placeholders are replaced at build time

volume_name = {{VOLUME_NAME}}
format = 'ULMO'
badge_icon = {{BADGE_ICON}}
background = {{BACKGROUND}}

# Volume size (None = auto-calculate)
size = None

# Files and symlinks
files = [{{APP_PATH}}]
symlinks = {
'Applications': '/Applications'
}

# Window settings
show_status_bar = False
show_tab_view = False
show_toolbar = False
show_pathbar = False
show_sidebar = False
sidebar_width = 180

# Window position and size
window_rect = ((100, 400), (480, 352))

# Icon view settings
default_view = 'icon-view'
icon_locations = {
{{APP_NAME}}: (120, 160),
'Applications': (360, 160)
}

# Text size for icon labels
text_size = 12
icon_size = 80
Loading
Loading