-
Notifications
You must be signed in to change notification settings - Fork 326
/
Copy pathsignArchivesMacOs.ts
385 lines (345 loc) · 14 KB
/
signArchivesMacOs.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
/**
* @file This script signs the content of all archives that we have for macOS.
* For this to work this needs to run on macOS with `codesign`, and a JDK installed.
* `codesign` is needed to sign the files, while the JDK is needed for correct packing
* and unpacking of java archives.
*
* We require this extra step as our dependencies contain files that require us
* to re-sign jar contents that cannot be opened as pure zip archives,
* but require a java toolchain to extract and re-assemble to preserve manifest information.
* This functionality is not provided by `electron-osx-sign` out of the box.
*
* This code is based on https://github.com/electron/electron-osx-sign/pull/231
* but our use-case is unlikely to be supported by `electron-osx-sign`
* as it adds a java toolchain as additional dependency.
* This script should be removed once the engine is signed.
*/
import * as childProcess from 'node:child_process'
import * as fs from 'node:fs/promises'
import * as os from 'node:os'
import * as pathModule from 'node:path'
import glob from 'fast-glob'
// ===============================================
// === Patterns of entities that need signing. ===
// ===============================================
/** Parts of the GraalVM distribution that need to be signed by us in an extra step. */
async function graalSignables(resourcesDir: string): Promise<Signable[]> {
const archivePatterns: ArchivePattern[] = [
['Contents/Home/jmods/java.base.jmod', ['bin/java', 'bin/keytool', 'lib/jspawnhelper']],
['Contents/Home/jmods/java.rmi.jmod', ['bin/rmiregistry']],
['Contents/Home/jmods/java.scripting.jmod', ['bin/jrunscript']],
['Contents/Home/jmods/jdk.compiler.jmod', ['bin/javac', 'bin/serialver']],
['Contents/Home/jmods/jdk.hotspot.agent.jmod', ['bin/jhsdb']],
['Contents/Home/jmods/jdk.httpserver.jmod', ['bin/jwebserver']],
['Contents/Home/jmods/jdk.jartool.jmod', ['bin/jarsigner', 'bin/jar']],
['Contents/Home/jmods/jdk.javadoc.jmod', ['bin/javadoc']],
['Contents/Home/jmods/jdk.javadoc.jmod', ['bin/javadoc']],
['Contents/Home/jmods/jdk.jconsole.jmod', ['bin/jconsole']],
['Contents/Home/jmods/jdk.jdeps.jmod', ['bin/javap', 'bin/jdeprscan', 'bin/jdeps']],
['Contents/Home/jmods/jdk.jdi.jmod', ['bin/jdb']],
['Contents/Home/jmods/jdk.jfr.jmod', ['bin/jfr']],
['Contents/Home/jmods/jdk.jlink.jmod', ['bin/jmod', 'bin/jlink', 'bin/jimage']],
['Contents/Home/jmods/jdk.jshell.jmod', ['bin/jshell']],
[
'Contents/Home/jmods/jdk.jpackage.jmod',
['bin/jpackage', 'classes/jdk/jpackage/internal/resources/jpackageapplauncher'],
],
['Contents/Home/jmods/jdk.jstatd.jmod', ['bin/jstatd']],
[
'Contents/Home/jmods/jdk.jcmd.jmod',
['bin/jstack', 'bin/jcmd', 'bin/jps', 'bin/jmap', 'bin/jstat', 'bin/jinfo'],
],
]
const binariesPatterns = ['Contents/MacOS/libjli.dylib']
// We use `*` for Graal versioned directory to not have to update this script on every GraalVM
// update. Updates might still be needed when the list of binaries to sign changes.
const graalDir = pathModule.join(resourcesDir, 'enso', 'runtime', '*')
const archives = await ArchiveToSign.lookupMany(graalDir, archivePatterns)
const binaries = await BinaryToSign.lookupMany(graalDir, binariesPatterns)
return [...archives, ...binaries]
}
/** Parts of the Enso Engine distribution that need to be signed by us in an extra step. */
async function ensoPackageSignables(resourcesDir: string): Promise<Signable[]> {
// Archives, and their content that need to be signed in an extra step. If a new archive is
// added to the engine dependencies this also needs to be added here. If an archive is not added
// here, it will show up as a failure to notarise the IDE. The offending archive will be named
// in the error message provided by Apple and can then be added here.
const engineDir = `${resourcesDir}/enso/dist/*`
const archivePatterns: ArchivePattern[] = [
[
'/component/runner/runner.jar',
[
'org/sqlite/native/Mac/x86_64/libsqlitejdbc.jnilib',
'org/sqlite/native/Mac/aarch64/libsqlitejdbc.jnilib',
'com/sun/jna/darwin-aarch64/libjnidispatch.jnilib',
'com/sun/jna/darwin-x86-64/libjnidispatch.jnilib',
],
],
[
'component/python-resources-*.jar',
[
'META-INF/resources/darwin/*/lib/graalpy*/*.dylib',
'META-INF/resources/darwin/*/lib/graalpy*/modules/*.so',
],
],
[
'component/truffle-nfi-libffi-*.jar',
['META-INF/resources/nfi-native/libnfi/darwin/*/bin/libtrufflenfi.dylib'],
],
[
'component/truffle-runtime-*.jar',
[
'META-INF/resources/engine/libtruffleattach/darwin/amd64/bin/libtruffleattach.dylib',
'META-INF/resources/engine/libtruffleattach/darwin/aarch64/bin/libtruffleattach.dylib',
],
],
['component/jna-*.jar', ['com/sun/jna/*/libjnidispatch.jnilib']],
[
'component/jline-native-*.jar',
[
'org/jline/nativ/Mac/arm64/libjlinenative.jnilib',
'org/jline/nativ/Mac/x86_64/libjlinenative.jnilib',
'org/jline/nativ/Mac/x86/libjlinenative.jnilib',
],
],
[
'lib/Standard/Database/*/polyglot/java/sqlite-jdbc-*.jar',
[
'org/sqlite/native/Mac/aarch64/libsqlitejdbc.jnilib',
'org/sqlite/native/Mac/x86_64/libsqlitejdbc.jnilib',
'org/sqlite/native/Mac/aarch64/libsqlitejdbc.dylib',
'org/sqlite/native/Mac/x86_64/libsqlitejdbc.dylib',
],
],
[
'lib/Standard/Snowflake/*/polyglot/java/snowflake-jdbc-*.jar',
[
'META-INF/native/libconscrypt_openjdk_jni-osx-*.dylib',
'META-INF/native/libio_grpc_netty_shaded_netty_tcnative_osx_*.jnilib',
],
],
[
'lib/Standard/Google_Api/*/polyglot/java/grpc-netty-shaded-*.jar',
['META-INF/native/libio_grpc_netty_shaded_netty_tcnative_osx_*.jnilib'],
],
[
'lib/Standard/Google_Api/*/polyglot/java/conscrypt-openjdk-uber-*.jar',
['META-INF/native/libconscrypt_openjdk_jni-osx-*.dylib'],
],
['lib/Standard/Tableau/*/polyglot/java/jna-*.jar', ['com/sun/jna/*/libjnidispatch.jnilib']],
]
const binariesPattern = 'lib/Standard/Image/*/polyglot/lib/*.dylib'
const binaries = await BinaryToSign.lookupMany(engineDir, [binariesPattern])
const archives = await ArchiveToSign.lookupMany(engineDir, archivePatterns)
return [...archives, ...binaries]
}
// ================
// === Signing. ===
// ================
/** Information we need to sign a given binary. */
interface SigningContext {
/**
* A digital identity that is stored in a keychain that is on the calling user's keychain
* search list. We rely on this already being set up by the Electron Builder.
*/
readonly identity: string
/** Path to the entitlements file. */
readonly entitlements: string
}
/** An entity that we want to sign. */
interface Signable {
/** Sign this entity. */
readonly sign: (context: SigningContext) => Promise<void>
}
/** Placeholder name for temporary archives. */
const TEMPORARY_ARCHIVE_PATH = 'temporary_archive.zip'
/** Helper to execute a program in a given directory and return the output. */
function run(cmd: string, args: string[], cwd?: string) {
console.log('Running', cmd, args, cwd)
return childProcess.execFileSync(cmd, args, { cwd }).toString()
}
/**
* Archive with some binaries that we want to sign.
*
* Can be either a zip or a jar file.
*/
class ArchiveToSign implements Signable {
/** Looks up for archives to sign using the given path patterns. */
static lookupMany = lookupManyHelper(ArchiveToSign.lookup.bind(this))
/** Create a new instance. */
constructor(
/** An absolute path to the archive. */
public path: string,
/**
* A list of patterns for files to sign inside the archive.
* Relative to the root of the archive.
*/
public binaries: glob.Pattern[],
) {}
/** Looks up for archives to sign using the given path pattern. */
static async lookup(base: string, [pattern, binaries]: ArchivePattern) {
return lookupHelper(path => new ArchiveToSign(path, binaries))(base, pattern)
}
/**
* Sign content of an archive. This function extracts the archive, signs the required files,
* re-packages the archive and replaces the original.
*/
async sign(context: SigningContext) {
console.log(`Signing archive ${this.path}`)
const archiveName = pathModule.basename(this.path)
const workingDir = await getTmpDir()
try {
const isJar = archiveName.endsWith('jar')
if (isJar) {
run('jar', ['xf', this.path], workingDir)
} else {
// We cannot use `unzip` here because of the following issue:
// https://unix.stackexchange.com/questions/115825/
// This started to be an issue with GraalVM 22.3.0 release.
run('7za', ['X', `-o${workingDir}`, this.path])
}
const binariesToSign = await BinaryToSign.lookupMany(workingDir, this.binaries)
for (const binaryToSign of binariesToSign) {
void binaryToSign.sign(context)
}
if (isJar) {
if (archiveName.includes('runner')) {
run('jar', ['-cfm', TEMPORARY_ARCHIVE_PATH, 'META-INF/MANIFEST.MF', '.'], workingDir)
} else {
run('jar', ['-cf', TEMPORARY_ARCHIVE_PATH, '.'], workingDir)
}
} else {
run('zip', ['-rm', TEMPORARY_ARCHIVE_PATH, '.'], workingDir)
}
// We cannot use fs.rename because temp and target might be on different volumes.
console.log(run('/bin/mv', [pathModule.join(workingDir, TEMPORARY_ARCHIVE_PATH), this.path]))
console.log(`Successfully repacked ${this.path} to handle signing inner native dependency.`)
return
} catch (error) {
console.error(
`Could not repackage ${archiveName}. Please check the ${import.meta.url} task to ` +
"ensure that it's working. This jar has to be treated specially " +
"because it has a native library and Apple's codesign does not sign inner " +
'native libraries correctly for jar files.',
)
throw error
} finally {
await rmRf(workingDir)
}
}
}
/** A single code binary file to be signed. */
class BinaryToSign implements Signable {
/** Looks up for binaries to sign using the given path pattern. */
static lookup = lookupHelper(path => new BinaryToSign(path))
/** Looks up for binaries to sign using the given path patterns. */
static lookupMany = lookupManyHelper(BinaryToSign.lookup)
/** Create a new instance. */
constructor(
/** An absolute path to the binary. */
public path: string,
) {}
/** Sign this binary. */
async sign({ entitlements, identity }: SigningContext) {
console.log(`Signing ${this.path}`)
run('codesign', [
'-vvv',
'--entitlements',
entitlements,
'--force',
'--options=runtime',
'--sign',
identity,
this.path,
])
// Async functions should contain await.
await Promise.resolve()
}
}
// ==============================
// === Discovering Signables. ===
// ==============================
/**
* Helper used to concisely define patterns for an archive to sign.
*
* Consists of pattern of the archive path
* and set of patterns for files to sign inside the archive.
*/
type ArchivePattern = [glob.Pattern, glob.Pattern[]]
/** Like `glob` but returns absolute paths by default. */
async function globAbsolute(pattern: glob.Pattern, options?: glob.Options): Promise<string[]> {
const paths = await glob(pattern, { absolute: true, ...options })
return paths
}
/**
* Glob patterns relative to a given base directory. The base directory is allowed to be a pattern
* as well.
*/
async function globAbsoluteIn(
base: glob.Pattern,
pattern: glob.Pattern,
options?: glob.Options,
): Promise<string[]> {
return globAbsolute(pathModule.join(base, pattern), options)
}
/** Generate a lookup function for a given Signable type. */
function lookupHelper<R extends Signable>(mapper: (path: string) => R) {
return async (base: string, pattern: glob.Pattern) => {
const paths = await globAbsoluteIn(base, pattern)
return paths.map(mapper)
}
}
/** Generate a lookup function for a given Signable type. */
function lookupManyHelper<T, R extends Signable>(
lookup: (base: string, pattern: T) => Promise<R[]>,
) {
return async function (base: string, patterns: T[]) {
const results = await Promise.all(
patterns.map(async pattern => {
const ret = await lookup(base, pattern)
if (ret.length === 0) {
console.warn(`No files found for pattern ${String(pattern)} in ${base}`)
}
return ret
}),
)
return results.flat()
}
}
// ==================
// === Utilities. ===
// ==================
/** Remove file recursively. */
async function rmRf(path: string) {
await fs.rm(path, { recursive: true, force: true })
}
/** Get a new temporary directory. Caller is responsible for cleaning up the directory. */
async function getTmpDir(prefix?: string) {
return await fs.mkdtemp(pathModule.join(os.tmpdir(), prefix ?? 'enso-signing-'))
}
// ====================
// === Entry point. ===
// ====================
/** Input for this script. */
interface Input extends SigningContext {
readonly appOutDir: string
readonly productFilename: string
}
/** Entry point, meant to be used from an afterSign Electron Builder's hook. */
export default async function (context: Input) {
console.log('Environment: ', process.env)
const { appOutDir, productFilename } = context
const appDir = pathModule.join(appOutDir, `${productFilename}.app`)
const contentsDir = pathModule.join(appDir, 'Contents')
const resourcesDir = pathModule.join(contentsDir, 'Resources')
// Sign archives.
console.log('Signing GraalVM elemenets...')
for (const signable of await graalSignables(resourcesDir)) await signable.sign(context)
console.log('Signing Engine elements...')
for (const signable of await ensoPackageSignables(resourcesDir)) await signable.sign(context)
// Finally re-sign the top-level enso.
const topLevelExecutable = new BinaryToSign(
pathModule.join(contentsDir, 'MacOS', productFilename),
)
await topLevelExecutable.sign(context)
}