-
Notifications
You must be signed in to change notification settings - Fork 26
/
Copy pathue4build.js
396 lines (363 loc) · 11.3 KB
/
ue4build.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
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
386
387
388
389
390
391
392
393
394
395
396
#!/usr/bin/env node
'use strict'
const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
const PLUGIN_DIR = 'Plugins'
const CONFIG_FILENAME = '.ue4build.json'
// load custom config
let config = {}
if (fs.existsSync(CONFIG_FILENAME)) {
config = JSON.parse(fs.readFileSync(CONFIG_FILENAME))
} else {
console.log(`${CONFIG_FILENAME} not found. Generating.`)
generateConfig()
process.exit(0)
}
// log helpers
let logfh = null
function log (data) {
if (!logfh) {
logfh = fs.openSync(config.buildLog[1], 'w')
}
fs.writeSync(logfh, data)
}
// the modified 'outputPath' with '_#' appended to avoid conflicts
let outputDir = null
// make sure we can read the project file before proceeding
let project = JSON.parse(fs.readFileSync(config.projectFile[1]))
let projectBackup = config.projectFile[1] + '.bak'
runBuild()
// entrypoint build sequence.
// disables all mod plugins
// executes the main project build
function runBuild () {
// first, backup the project file
try { fs.unlinkSync(projectBackup) } catch (e) { /* pass */ }
copyFile(config.projectFile[1], projectBackup).then(() => {
// disable all dlc/mod plugins
console.log('Disabling all mod plugins')
for (let modPlug of config.modPlugins[1]) {
if (modPlug.asMod[1]) {
setPluginEnabled(modPlug.name[1], false)
}
}
fs.writeFileSync(config.projectFile[1], JSON.stringify(project, null, '\t'))
console.log('= Execute Main Build =')
runUAT(getMainBuildParams()).then(runBuildStep2, (err) => {
console.error('Exited with code: ' + err.code)
process.exit(1)
})
})
}
// step 2 of the build sequence
// re-enables all mod plugins
// executes individual mod(dlc) builds in sequence
function runBuildStep2 () {
// re-enable all dlc/mod plugins
console.log('Reenabling all mod plugins')
for (let modPlug of config.modPlugins[1]) {
if (modPlug.asMod[1]) {
setPluginEnabled(modPlug.name[1], true)
}
}
fs.writeFileSync(config.projectFile[1], JSON.stringify(project, null, '\t'))
// build all the mods
let modsToBuild = []
for (let modPlug of config.modPlugins[1]) {
if (modPlug.asMod[1]) {
modsToBuild.push(modPlug.name[1])
}
}
let buildNext = () => {
if (!modsToBuild.length) {
runBuildStep3()
return
}
let modName = modsToBuild.shift()
console.log('= Execute Mod Build - ' + modName + ' =')
runUAT(getModBuildParams(modName)).then(buildNext, (err) => {
// check for update resource bug
if (err.result.indexOf("Program.Main: ERROR: AutomationTool terminated with exception: System.Exception: Couldn't update resource") > -1 && err.result.indexOf('Project.RunUnrealPak: UnrealPak Done') > -1) {
log('^-- NOTE: this is an editor bug... the build failed but not before generating\nthe needed .pak and AssetRegistry.bin files. Everything is OK, proceeding...\n')
buildNext()
return
}
console.error('Exited with code: ' + err.code)
process.exit(1)
})
}
buildNext()
}
// step 3 of the build sequence
// copies the main build to the output dir
function runBuildStep3 () {
let source = path.resolve(path.normalize(`Saved/StagedBuilds/${config.platformDirName[1]}`))
let dest = config.outputPath[1]
outputDir = dest
let num = 1
while (fs.existsSync(outputDir)) {
outputDir = dest + '_' + num
++num
}
console.log('Copying ' + source + ' to ' + outputDir)
copyDir(source, outputDir).then(runBuildStep4, (err) => {
console.error(err)
process.exit(1)
})
}
// step 4 of the build sequence
// copies all mod .pak and .bin files to the output dir
function runBuildStep4 () {
// build all the mods
let modsToCopy = []
for (let modPlug of config.modPlugins[1]) {
if (modPlug.asMod[1]) {
modsToCopy.push(modPlug.name[1])
}
}
let buildNext = () => {
if (!modsToCopy.length) {
runBuildStep5()
return
}
let modName = modsToCopy.shift()
let all = []
let source = path.resolve(path.normalize(`Plugins/${modName}/Saved/Cooked/${config.platformDirName[1]}/${config.projectName[1]}/AssetRegistry.bin`))
let dest = path.resolve(path.normalize(`${outputDir}/${config.projectName[1]}/Content/Paks/${modName}.bin`))
console.log('Copying ' + source + ' to ' + dest)
all.push(copyFile(source, dest))
source = path.resolve(path.normalize(`Plugins/${modName}/Saved/StagedBuilds/${config.platformDirName[1]}/${config.projectName[1]}/Content/Paks/${config.projectName[1]}-${config.platformDirName[1]}.pak`))
dest = path.resolve(path.normalize(`${outputDir}/${config.projectName[1]}/Content/Paks/${modName}.pak`))
console.log('Copying ' + source + ' to ' + dest)
all.push(copyFile(source, dest))
Promise.all(all).then(() => {
buildNext()
}, (err) => {
console.error(err)
process.exit(1)
})
}
buildNext()
}
// step 5 of the build sequence
// cleans up backup files and closes the log file
function runBuildStep5 () {
// cleanup backup file
try { fs.unlinkSync(projectBackup) } catch (e) { /* pass */ }
if (logfh) {
fs.closeSync(logfh)
logfh = null
}
console.log('ue4build Complete. -> ' + outputDir)
}
// search through the .uproject json to enable/disable a plugin
function setPluginEnabled (name, enabled) {
if (!('Plugins' in project)) {
project.Plugins = []
}
for (let plug of project.Plugins) {
if (plug.Name === name) {
plug.Enabled = enabled
return
}
}
project.Plugins.push({
Name: name,
Enabled: enabled
})
}
// if we don't have a config yet... scan the directory and build a default
function generateConfig () {
const MOD_TPL = {
name: ['# Name of the Mod (DLC) plugin', ''],
asMod: ["# if 'true' will be handled as a mod", false]
}
config = {
projectName: ['# the project name', ''],
projectFile: ['# the .uproject file to build', ''],
buildConfig: ["# 'DebugGame', 'Development', or 'Shipping'", 'Development'],
targetPlatform: ['# Unreal build target platform', 'Win64'],
platformDirName: ['# The name unreal will give to StagedBuild platform directories', 'WindowsNoEditor'],
buildLog: ['# where to output AutomationTool.exe messages', '.ue4build.log'],
outputPath: ['# will copy the build tree to this destination', 'Releases/ModBuild'],
uatCommand: ['# path to the RunUAT batch or shell script', '/path/to/RunUAT'],
modPlugins: ['# list of plugins to treat as dlc mods', []]
}
config.outputPath[1] = path.resolve(path.normalize(config.outputPath[1]))
// determine project name
let projectList = fs.readdirSync('.').filter((item) => {
if (fs.statSync(item).isFile() && path.extname(item) === '.uproject') {
return true
}
})
if (projectList.length) {
config.projectFile[1] = path.resolve(path.normalize(projectList[0]))
}
config.projectName[1] = path.parse(config.projectFile[1]).name
// determine engine version
let project = JSON.parse(fs.readFileSync(config.projectFile[1]))
let engineVersion = project.EngineAssociation
let disabledPlugins = {}
if ('Plugins' in project) {
for (let p of project.Plugins) {
if (p.Enabled === false) {
disabledPlugins[p.Name] = true
}
}
}
// determine target platform
switch (process.platform) {
case 'darwin':
config.targetPlatform[1] = 'Darwin'
config.platformDirName[1] = 'MacNoEditor'
break
default:
config.targetPlatform[1] = 'Win64'
config.platformDirName[1] = 'WindowsNoEditor'
break
}
// determine uat path
let uatSearch = [
path.resolve(path.normalize(`C:\\Program Files\\Epic Games\\UE_${engineVersion}\\Engine\\Build\\BatchFiles\\RunUAT.bat`))
]
for (let cmd of uatSearch) {
if (fs.existsSync(cmd)) {
config.uatCommand[1] = cmd
break
}
}
// scan for plugins
let pluginList = fs.readdirSync(PLUGIN_DIR).filter((item) => {
if (fs.statSync(path.join(PLUGIN_DIR, item)).isDirectory()) {
return true
}
})
let modPlugins = config.modPlugins[1]
for (let pluginName of pluginList) {
let mod = JSON.parse(JSON.stringify(MOD_TPL))
mod.name[1] = pluginName
mod.asMod[1] = !disabledPlugins[pluginName]
modPlugins.push(mod)
}
// finish up
fs.writeFileSync(CONFIG_FILENAME, JSON.stringify(config, null, ' '))
console.log('generated config, please double check that everything is correct:')
console.log(JSON.stringify(config, null, ' '))
}
// RunUAT parameters for doing a main project build
function getMainBuildParams () {
return [
'BuildCookRun',
'-project="' + config.projectFile[1] + '"',
'-noP4',
'-clientconfig=' + config.buildConfig[1],
'-serverconfig=' + config.buildConfig[1],
'-nocompile',
'-nocompileeditor',
'-installed',
'-ue4exe=UE4Editor-Cmd.exe',
'-utf8output',
'-platform=' + config.targetPlatform[1],
'-targetplatform=' + config.targetPlatform[1],
'-build',
'-cook',
'-map=',
'-pak',
'-createreleaseversion=1.0',
'-compressed',
'-stage',
'-package'
]
}
// RunUAT parameters for doing a mod(dlc) build
function getModBuildParams (modName) {
return [
'BuildCookRun',
'-project="' + config.projectFile[1] + '"',
'-noP4',
'-clientconfig=' + config.buildConfig[1],
'-serverconfig=' + config.buildConfig[1],
'-nocompile',
'-nocompileeditor',
'-installed',
'-ue4exe=UE4Editor-Cmd.exe',
'-utf8output',
'-platform=' + config.targetPlatform[1],
'-targetplatform=' + config.targetPlatform[1],
'-build',
'-cook',
'-map=',
'-pak',
'-dlcname=' + modName,
'-basedonreleaseversion=1.0',
'-compressed',
'-stage',
'-package'
]
}
// Execute the actual RunUAT process in a sub-shell-process
function runUAT (args) {
let result = ''
return new Promise((resolve, reject) => {
let cmd = '"' + config.uatCommand[1] + '"'
console.log(cmd + ' ' + args.join(' '))
let proc = childProcess.spawn(cmd, args, {
shell: true
})
proc.stdout.on('data', (data) => {
result += data.toString()
log(data.toString())
process.stdout.write('.')
})
proc.stderr.on('data', (data) => {
process.stderr.write(data)
})
proc.on('close', (code) => {
process.stdout.write('\n')
if (code === 0) {
resolve(result)
} else {
let err = new Error()
err.code = code
err.result = result
reject(err)
}
})
})
}
// copy a single file
function copyFile (source, target) {
return new Promise((resolve, reject) => {
var rd = fs.createReadStream(source)
rd.on('error', rejectCleanup)
var wr = fs.createWriteStream(target)
wr.on('error', rejectCleanup)
function rejectCleanup (err) {
rd.destroy()
wr.end()
reject(err)
}
wr.on('finish', resolve)
rd.pipe(wr)
})
}
// recursively copy a directory
function copyDir (source, target) {
return new Promise((resolve, reject) => {
let all = []
fs.mkdirSync(target)
let sub = fs.readdirSync(source)
for (let file of sub) {
let subtarget = path.join(target, file)
file = path.join(source, file)
let stat = fs.statSync(file)
if (stat.isDirectory()) {
all.push(copyDir(file, subtarget))
} else if (stat.isFile()) {
all.push(copyFile(file, subtarget))
}
}
Promise.all(all).then(resolve, reject)
})
}