Skip to content

Commit 8d07e72

Browse files
authored
Try MacOS build (#91)
1 parent ba0d3a9 commit 8d07e72

File tree

10 files changed

+347
-59
lines changed

10 files changed

+347
-59
lines changed

.erb/scripts/beforePack.js

Lines changed: 146 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,42 @@ const JULIA_VERSION_PARTS = [1, 10, 10];
1818
const JULIA_VERSION = JULIA_VERSION_PARTS.join('.');
1919
const JULIA_VERSION_MINOR = JULIA_VERSION_PARTS.slice(0, 2).join('.');
2020

21-
const JULIA_URL = `https://julialang-s3.julialang.org/bin/winnt/x64/${JULIA_VERSION_MINOR}/julia-${JULIA_VERSION}-win64.zip`;
21+
// Detect platform
22+
const platform = process.platform;
23+
const arch = process.arch === 'arm64' ? 'aarch64' : 'x64';
2224

23-
const ZIP_NAME = `julia-${JULIA_VERSION}-win64.zip`;
24-
const JULIA_DIR_NAME = `julia-${JULIA_VERSION}`;
25+
let JULIA_URL, ZIP_NAME, JULIA_DIR_NAME, JULIA_EXECUTABLE;
26+
27+
if (platform === 'win32') {
28+
ZIP_NAME = `julia-${JULIA_VERSION}-win64.zip`;
29+
JULIA_URL = `https://julialang-s3.julialang.org/bin/winnt/x64/${JULIA_VERSION_MINOR}/${ZIP_NAME}`;
30+
JULIA_DIR_NAME = `julia-${JULIA_VERSION}`;
31+
JULIA_EXECUTABLE = 'julia.exe';
32+
} else if (platform === 'darwin') {
33+
// macOS
34+
const macArch = arch === 'aarch64' ? 'aarch64' : 'x64';
35+
ZIP_NAME = `julia-${JULIA_VERSION}-mac${arch === 'aarch64' ? 'aarch64' : '64'}.dmg`;
36+
JULIA_URL = `https://julialang-s3.julialang.org/bin/mac/${macArch}/${JULIA_VERSION_MINOR}/${ZIP_NAME}`;
37+
JULIA_DIR_NAME = `julia-${JULIA_VERSION}`;
38+
JULIA_EXECUTABLE = 'julia';
39+
} else {
40+
// Linux
41+
throw new Error('Linux is not supported');
42+
}
2543

2644
const DEPOT_NAME = `julia_depot`;
2745

2846
const downloadJulia = async () => {
29-
const spinner = createSpinner(`\tDownloading Julia ${JULIA_VERSION}`).start();
47+
const spinner = createSpinner(
48+
`\tDownloading Julia ${JULIA_VERSION} for ${platform}`,
49+
).start();
3050
const writer = fs.createWriteStream(path.join(assetPath, ZIP_NAME));
3151

3252
const response = await axios.get(JULIA_URL, {
3353
responseType: 'stream',
3454
onDownloadProgress: (progressEvent) => {
3555
const percentage = Math.round(
36-
(progressEvent.loaded * 100) / progressEvent.total
56+
(progressEvent.loaded * 100) / progressEvent.total,
3757
);
3858
spinner
3959
.update({ text: `\tDownloading Julia ${JULIA_VERSION} ${percentage}%` })
@@ -56,17 +76,22 @@ const downloadJulia = async () => {
5676
reject(e);
5777
});
5878
writer.on('finish', (args) => {
59-
spinner.success({ text: '\tDownloaded Julia', mark: '✓' });
79+
spinner.success({
80+
text: `\tDownloaded Julia (for ${platform}) (size: ${(response.data.length / 1024 / 1024).toFixed(2)} MB)`,
81+
mark: '✓',
82+
});
6083
resolve(args);
6184
});
6285
});
6386
};
6487

6588
const precompilePluto = async ({ julia_path }) => {
89+
// TODO: You need to add PackageCompiler to some environment for this to work.
90+
6691
const SYSIMAGE_LOCATION = path.join(
6792
assetPath,
6893
// TODO: auto version number
69-
'pluto.so'
94+
'pluto.so',
7095
);
7196

7297
// immediately return if the sysimage has already been compiled
@@ -77,7 +102,7 @@ const precompilePluto = async ({ julia_path }) => {
77102
const PRECOMPILE_SCRIPT_LOCATION = path.join(assetPath, 'precompile.jl');
78103
const PRECOMPILE_EXECUTION_LOCATION = path.join(
79104
assetPath,
80-
'precompile_execution.jl'
105+
'precompile_execution.jl',
81106
);
82107

83108
const res = spawn(julia_path, [
@@ -124,7 +149,7 @@ const prepareJuliaDepot = async ({ julia_path }) => {
124149
env: {
125150
JULIA_DEPOT_PATH: DEPOT_LOCATION,
126151
},
127-
}
152+
},
128153
);
129154

130155
res.stderr.on('data', (data) => {
@@ -146,35 +171,130 @@ const prepareJuliaDepot = async ({ julia_path }) => {
146171
recursive: true,
147172
});
148173

174+
// Fix file permissions on macOS to prevent code signing issues
175+
if (platform === 'darwin') {
176+
// (🤖🤖 This if block is AI written and not reviewed.)
177+
try {
178+
// First, make files writable so we can remove extended attributes
179+
execSync(`find "${DEPOT_LOCATION}" -type f -exec chmod u+w {} +`, {
180+
stdio: 'ignore',
181+
});
182+
execSync(`find "${DEPOT_LOCATION}" -type d -exec chmod u+w {} +`, {
183+
stdio: 'ignore',
184+
});
185+
// Remove extended attributes (quarantine, etc.) that can interfere with code signing
186+
execSync(`xattr -rc "${DEPOT_LOCATION}" 2>/dev/null || true`, {
187+
stdio: 'ignore',
188+
});
189+
// Set final correct permissions: 644 for files, 755 for directories
190+
execSync(`find "${DEPOT_LOCATION}" -type f -exec chmod 644 {} +`, {
191+
stdio: 'ignore',
192+
});
193+
execSync(`find "${DEPOT_LOCATION}" -type d -exec chmod 755 {} +`, {
194+
stdio: 'ignore',
195+
});
196+
console.info(
197+
'Fixed file permissions and removed extended attributes in DEPOT',
198+
);
199+
} catch (error) {
200+
console.warn('Warning: Could not fix file permissions:', error.message);
201+
}
202+
}
203+
149204
console.info('DEPOT preparation success', DEPOT_LOCATION);
150205
};
151206

207+
/** (🤖🤖 This function is AI written and not reviewed.) */
208+
const extractJulia = async () => {
209+
const spinner1 = createSpinner(`\tExtracting: ${ZIP_NAME}`).start();
210+
fs.rmSync(path.join(assetPath, JULIA_DIR_NAME), {
211+
force: true,
212+
recursive: true,
213+
});
214+
215+
if (platform === 'win32') {
216+
// Windows: extract zip
217+
await unzip(path.join(assetPath, ZIP_NAME), { dir: assetPath });
218+
} else if (platform === 'darwin') {
219+
// macOS: mount DMG and copy contents
220+
const dmgPath = path.join(assetPath, ZIP_NAME);
221+
const mountPoint = path.join(assetPath, 'julia_mount');
222+
223+
// Create mount point if it doesn't exist
224+
if (!fs.existsSync(mountPoint)) {
225+
fs.mkdirSync(mountPoint, { recursive: true });
226+
}
227+
228+
// Mount the DMG
229+
execSync(`hdiutil attach "${dmgPath}" -mountpoint "${mountPoint}" -quiet`);
230+
231+
try {
232+
// Find the Julia app bundle in the mounted DMG
233+
const mountedContents = fs.readdirSync(mountPoint);
234+
const juliaApp = mountedContents.find((item) =>
235+
item.startsWith('Julia-'),
236+
);
237+
238+
if (!juliaApp) {
239+
throw new Error('Could not find Julia app in DMG');
240+
}
241+
242+
const juliaAppPath = path.join(mountPoint, juliaApp);
243+
const juliaContentsPath = path.join(
244+
juliaAppPath,
245+
'Contents',
246+
'Resources',
247+
'julia',
248+
);
249+
const targetPath = path.join(assetPath, JULIA_DIR_NAME);
250+
251+
// Copy Julia contents to target directory
252+
if (fs.existsSync(juliaContentsPath)) {
253+
// Copy the entire julia directory
254+
execSync(`cp -R "${juliaContentsPath}" "${targetPath}"`);
255+
} else {
256+
throw new Error('Could not find Julia contents in app bundle');
257+
}
258+
} finally {
259+
// Unmount the DMG
260+
execSync(`hdiutil detach "${mountPoint}" -quiet`);
261+
if (fs.existsSync(mountPoint)) {
262+
fs.rmSync(mountPoint, { recursive: true, force: true });
263+
}
264+
}
265+
} else {
266+
throw new Error('Linux is not supported');
267+
// Linux: extract tar.gz
268+
execSync(`tar -xzf "${path.join(assetPath, ZIP_NAME)}" -C "${assetPath}"`);
269+
}
270+
271+
fs.rmSync(path.join(assetPath, ZIP_NAME), {
272+
force: true,
273+
});
274+
spinner1.success({ text: '\tExtracted!', mark: '✓' });
275+
};
276+
152277
exports.default = async (context) => {
153278
let files = fs.readdirSync(assetPath);
154279

155280
if (!files.includes(JULIA_DIR_NAME)) {
156281
await downloadJulia();
157-
// files = fs.readdirSync(assetPath);
158-
159-
const spinner1 = createSpinner(`\tExtracting: ${ZIP_NAME}`).start();
160-
fs.rmSync(path.join(assetPath, JULIA_DIR_NAME), {
161-
force: true,
162-
recursive: true,
163-
});
164-
await unzip(path.join(assetPath, ZIP_NAME), { dir: assetPath });
165-
fs.rmSync(path.join(assetPath, ZIP_NAME), {
166-
force: true,
167-
});
168-
spinner1.success({ text: '\tExtracted!', mark: '✓' });
282+
await extractJulia();
169283
}
170284

285+
const juliaPath = path.join(
286+
assetPath,
287+
JULIA_DIR_NAME,
288+
'bin',
289+
JULIA_EXECUTABLE,
290+
);
291+
171292
await prepareJuliaDepot({
172-
julia_path: path.join(assetPath, JULIA_DIR_NAME, 'bin', 'julia.exe'),
293+
julia_path: juliaPath,
173294
});
174295

175296
// NOT DOING THIS, see https://github.com/JuliaPluto/PlutoDesktop/issues/56
176-
// maybe we are... (CB)
177-
await precompilePluto({
178-
julia_path: path.join(assetPath, JULIA_DIR_NAME, 'bin', 'julia.exe'),
179-
});
297+
// await precompilePluto({
298+
// julia_path: juliaPath,
299+
// });
180300
};

.erb/scripts/beforeSign.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
3+
4+
5+
6+
7+
🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖🤖
8+
9+
This file is completely written by AI and not really reviewed. It's not working super well.
10+
11+
12+
13+
*/
14+
15+
const { execSync } = require('child_process');
16+
const path = require('path');
17+
const fs = require('fs');
18+
19+
/**
20+
* Ensures all components of the macOS app are signed consistently with ad-hoc signature
21+
* This is necessary when no Developer ID certificate is available
22+
* This runs in the afterPack hook, which executes after packaging but before electron-builder's signing step
23+
*/
24+
exports.default = async function afterPack(context) {
25+
const { electronPlatformName, appOutDir } = context;
26+
27+
if (electronPlatformName !== 'darwin') {
28+
return;
29+
}
30+
31+
const appName = context.packager.appInfo.productFilename;
32+
const appPath = path.join(appOutDir, `${appName}.app`);
33+
34+
if (!fs.existsSync(appPath)) {
35+
console.warn('App bundle not found, skipping beforeSign');
36+
return;
37+
}
38+
39+
// Check if we have a valid Developer ID certificate
40+
try {
41+
const identities = execSync('security find-identity -v -p codesigning', {
42+
encoding: 'utf8',
43+
});
44+
const hasValidIdentity = identities.includes('Developer ID Application');
45+
46+
if (hasValidIdentity) {
47+
// Valid certificate exists, let electron-builder handle signing normally
48+
return;
49+
}
50+
} catch (error) {
51+
// No valid certificate found, proceed with ad-hoc signing
52+
}
53+
54+
console.log(
55+
'No valid Developer ID certificate found, ensuring ad-hoc signing...',
56+
);
57+
58+
const frameworksPath = path.join(appPath, 'Contents', 'Frameworks');
59+
const electronFrameworkPath = path.join(
60+
frameworksPath,
61+
'Electron Framework.framework',
62+
);
63+
64+
// Sign Electron Framework first (deep signing)
65+
if (fs.existsSync(electronFrameworkPath)) {
66+
try {
67+
// Sign the framework binary first
68+
const frameworkBinary = path.join(
69+
electronFrameworkPath,
70+
'Versions',
71+
'A',
72+
'Electron Framework',
73+
);
74+
if (fs.existsSync(frameworkBinary)) {
75+
execSync(
76+
`codesign --force --sign - --timestamp=none --options runtime "${frameworkBinary}"`,
77+
{ stdio: 'inherit' },
78+
);
79+
}
80+
// Then sign the framework bundle
81+
execSync(
82+
`codesign --force --sign - --timestamp=none "${electronFrameworkPath}"`,
83+
{ stdio: 'inherit' },
84+
);
85+
console.log('Signed Electron Framework with ad-hoc signature');
86+
} catch (error) {
87+
console.warn('Failed to sign Electron Framework:', error.message);
88+
}
89+
}
90+
91+
// Sign all other frameworks
92+
if (fs.existsSync(frameworksPath)) {
93+
const frameworks = fs.readdirSync(frameworksPath);
94+
for (const framework of frameworks) {
95+
if (framework === 'Electron Framework.framework') {
96+
continue; // Already handled above
97+
}
98+
99+
const frameworkPath = path.join(frameworksPath, framework);
100+
if (
101+
fs.statSync(frameworkPath).isDirectory() &&
102+
framework.endsWith('.framework')
103+
) {
104+
const binaryName = framework.replace('.framework', '');
105+
const binaryPath = path.join(frameworkPath, binaryName);
106+
if (fs.existsSync(binaryPath)) {
107+
try {
108+
execSync(
109+
`codesign --force --sign - --timestamp=none --options runtime "${binaryPath}"`,
110+
{ stdio: 'inherit' },
111+
);
112+
execSync(
113+
`codesign --force --sign - --timestamp=none "${frameworkPath}"`,
114+
{ stdio: 'inherit' },
115+
);
116+
console.log(`Signed ${framework} with ad-hoc signature`);
117+
} catch (error) {
118+
console.warn(`Failed to sign ${framework}:`, error.message);
119+
}
120+
}
121+
}
122+
}
123+
}
124+
125+
// Sign the main executable
126+
const mainExecutable = path.join(appPath, 'Contents', 'MacOS', appName);
127+
if (fs.existsSync(mainExecutable)) {
128+
try {
129+
execSync(
130+
`codesign --force --sign - --timestamp=none --options runtime "${mainExecutable}"`,
131+
{ stdio: 'inherit' },
132+
);
133+
console.log('Signed main executable with ad-hoc signature');
134+
} catch (error) {
135+
console.warn('Failed to sign main executable:', error.message);
136+
}
137+
}
138+
139+
// Finally, sign the entire app bundle (this ensures all components have matching Team IDs)
140+
// Note: We don't use --deep as it's deprecated, and we've already signed all components
141+
try {
142+
execSync(
143+
`codesign --force --sign - --timestamp=none --options runtime "${appPath}"`,
144+
{ stdio: 'inherit' },
145+
);
146+
console.log('Signed entire app bundle with ad-hoc signature');
147+
} catch (error) {
148+
console.warn('Failed to sign app bundle:', error.message);
149+
}
150+
};

0 commit comments

Comments
 (0)