-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathmain.js
233 lines (220 loc) · 7.62 KB
/
main.js
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
const path = require('path')
const util = require('util')
const os = require('os')
const fs = require('fs-extra')
const spawn = require('await-spawn')
const tar = require('tar')
const uglify = require('uglify-js')
const {PassThrough, Transform} = require('stream')
const {fixedFilterList, stockFilterList} = require('./filters')
const {ignoreError, cat, getYode} = require('./util')
const checkLicense = util.promisify(require('license-checker-rseidelsohn').init)
// Minimize js file.
function transform(rootDir, p) {
if (!p.endsWith('.js'))
return new PassThrough()
let data = ''
return new Transform({
transform(chunk, encoding, callback) {
data += chunk
callback(null)
},
flush(callback) {
const result = uglify.minify(data, {parse: {bare_returns: true}})
if (result.error) {
const rp = path.relative(rootDir, p)
const message = `${result.error.message} at line ${result.error.line} col ${result.error.col}`
console.error(`Failed to minify ${rp}:`, message)
callback(null, data)
} else {
callback(null, result.code)
}
}
})
}
// Parse the packageJson and generate app information.
function getAppInfo(packageJson) {
const appInfo = {
name: packageJson.name,
version: packageJson.version,
description: packageJson.description,
appId: `com.${packageJson.name}.${packageJson.name}`,
productName: packageJson.build.productName
}
if (packageJson.build)
Object.assign(appInfo, packageJson.build)
if (!appInfo.copyright)
appInfo.copyright = `Copyright © ${(new Date()).getYear()} ${appInfo.productName}`
return appInfo
}
// Copy the app into a dir and install production dependencies.
async function installApp(appDir, platform, arch) {
const immediateDir = await fs.mkdtemp(path.join(os.tmpdir(), 'yacakge-'))
const freshDir = path.join(immediateDir, 'package')
try {
// cd appDir && npm pack
const pack = await spawn('npm', ['pack'], {shell: true, cwd: appDir})
const tarball = path.join(appDir, pack.toString().trim())
try {
// cd immediateDir && tar xf tarball
await tar.x({file: tarball, cwd: immediateDir})
} finally {
await ignoreError(fs.remove(tarball))
}
// cd freshDir && npm install --production
const env = Object.create(process.env)
env.npm_config_platform = platform
env.npm_config_arch = arch
try {
await spawn('npm', ['install', '--production'], {shell: true, cwd: freshDir, env})
} catch (error) {
throw Error(`Failed to install app: \n${error.stderr}`)
}
} catch (error) {
console.error('Package dir left for debug:', freshDir)
throw error
}
return freshDir
}
// Append ASAR meta information at end of target.
async function appendMeta(target) {
const stat = await fs.stat(target)
const meta = Buffer.alloc(8 + 1 + 4)
const asarSize = stat.size + meta.length
meta.writeDoubleLE(asarSize, 0)
meta.writeUInt8(2, 8)
meta.write('ASAR', 9)
await fs.appendFile(target, meta)
}
// Creaet asar archive.
async function createAsarArchive(appDir, outputDir, target, options) {
const asarOpts = {
// Let glob search "**/*' under appDir, instead of passing appDir to asar
// directly. In this way our filters can work on the source root dir.
pattern: '**/*',
transform: options.minify ? transform.bind(this, appDir) : null,
unpack: options.unpack,
globOptions: {
cwd: appDir,
noDir: true,
ignore: fixedFilterList.concat(stockFilterList),
},
}
// Do not include outputDir in the archive.
let relativeOutputDir = path.isAbsolute(outputDir) ? path.relative(appDir, outputDir)
: outputDir
asarOpts.globOptions.ignore.push(outputDir)
asarOpts.globOptions.ignore.push(outputDir + '/*')
// Run asar under appDir to work around buggy glob behavior.
const cwd = process.cwd()
try {
process.chdir(appDir)
await require('asar').createPackageWithOptions('', target, asarOpts)
} finally {
process.chdir(cwd)
}
await appendMeta(target)
}
// Write the size of binary into binary.
async function replaceOffsetPlaceholder(target) {
const mark = '/* REPLACE_WITH_OFFSET */'
const data = await fs.readFile(target)
const pos = data.indexOf(Buffer.from(mark))
if (pos <= 0)
return false
const stat = await fs.stat(target)
const replace = `, ${stat.size}`.padEnd(mark.length, ' ')
data.write(replace, pos)
await fs.writeFile(target, data)
return true
}
// Collect licenses.
async function writeLicenseFile(outputDir, appDir) {
let license = ''
const data = await checkLicense({start: appDir})
for (const name in data) {
const info = data[name]
if (!info.licenseFile)
continue
license += name + '\n'
if (info.publisher)
license += info.publisher + '\n'
if (info.email)
license += info.email + '\n'
if (info.url)
license += info.url + '\n'
const content = await fs.readFile(info.licenseFile)
license += '\n' + content.toString().replace(/\r\n/g, '\n')
license += '\n' + '-'.repeat(70) + '\n\n'
}
await fs.writeFile(path.join(outputDir, 'LICENSE'), license)
}
// Sign all binaries in the res dir.
async function signAllBinaries(dir, top = true) {
const files = await fs.readdir(dir)
const promises = []
for (const f of files) {
const p = path.join(dir, f)
const stats = await fs.stat(p)
if (stats.isDirectory())
promises.push(signAllBinaries(p, false))
else if (stats.isFile() && f.endsWith('.node'))
promises.push(require('./mac').adHocSign(p))
}
await Promise.all(promises)
}
async function packageApp(outputDir, appDir, options, platform, arch) {
const appInfo = getAppInfo(await fs.readJson(path.join(appDir, 'package.json')))
Object.assign(options, appInfo)
await fs.emptyDir(outputDir)
let target = path.join(outputDir, platform === 'win32' ? `${appInfo.name}.exe` : appInfo.name)
const intermediateAsar = path.resolve(outputDir, 'app.ear')
const srcYodePath = getYode(appDir).path
const yodePath = path.resolve(outputDir, path.basename(srcYodePath))
try {
let canBeSigned
await Promise.all([
createAsarArchive(appDir, outputDir, intermediateAsar, options),
(async () => {
await fs.copy(srcYodePath, yodePath)
if (platform === 'win32')
await require('./win').modifyExe(yodePath, appInfo, appDir)
canBeSigned = await replaceOffsetPlaceholder(yodePath)
})(),
])
await cat(target, yodePath, intermediateAsar)
if (canBeSigned && platform === 'darwin') {
await require('./mac').extendStringTableSize(target)
await require('./mac').adHocSign(target)
}
await Promise.all([
fs.chmod(target, 0o755),
writeLicenseFile(outputDir, appDir),
(async () => {
const resDir = path.join(outputDir, 'res')
await ignoreError(fs.remove(resDir))
await ignoreError(fs.rename(`${intermediateAsar}.unpacked`, resDir))
if (canBeSigned && platform === 'darwin')
await signAllBinaries(resDir)
})(),
])
} finally {
await ignoreError([
fs.remove(yodePath),
fs.remove(intermediateAsar),
fs.remove(`${intermediateAsar}.unpacked`),
])
}
if (platform === 'darwin')
target = await require('./mac').createBundle(appInfo, appDir, outputDir, target)
return target
}
async function packageCleanApp(outputDir, appDir, options, platform, arch) {
const freshDir = await installApp(appDir, platform, arch)
try {
return await packageApp(outputDir, freshDir, options, platform, arch)
} finally {
await ignoreError(fs.remove(freshDir))
}
}
module.exports = {packageApp, packageCleanApp}