[MIRROR] Juke Build (#6313)

* Juke Build (#59390)

* Juke Build Hotfix 1 (#59643)

* Juke Build Fix

* More fixes

* Juke Build Hotfix 2 - PreCompile script compatibility (#59649)

* Juke Build Hotfix 2 - PreCompile script compatibility

* Pass arguments from bat to build.js

* Pass arguments in BUILD.bat as well

* Quick tweak

* Modular Skyrat detection~

Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
This commit is contained in:
Funce
2021-06-14 22:14:29 +12:00
committed by GitHub
parent d3226446db
commit df9e8183f6
22 changed files with 7624 additions and 3816 deletions

View File

@@ -58,9 +58,7 @@ jobs:
bash tools/ci/install_byond.sh
source $HOME/BYOND/byond/bin/byondsetup
python3 tools/ci/template_dm_generator.py
tools/build/build
env:
CBT_BUILD_MODE : ALL_MAPS
tools/build/build dm -DCIBUILDING -DCITESTING -DALL_MAPS
run_all_tests:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@@ -98,10 +96,8 @@ jobs:
run: |
bash tools/ci/install_byond.sh
source $HOME/BYOND/byond/bin/byondsetup
tools/build/build
tools/build/build -DCIBUILDING
bash tools/ci/run_server.sh
env:
CBT_BUILD_MODE: TEST_RUN
test_windows:
if: "!contains(github.event.head_commit.message, '[ci skip]')"

View File

@@ -1,2 +1,3 @@
@call "%~dp0\tools\build\build"
@pause
@echo off
call "%~dp0\tools\build\build.bat" %*
pause

View File

@@ -3,21 +3,21 @@
/datum/asset/simple/tgui_common
keep_local_name = TRUE
assets = list(
"tgui-common.bundle.js" = 'tgui/public/tgui-common.bundle.js',
"tgui-common.bundle.js" = file("tgui/public/tgui-common.bundle.js"),
)
/datum/asset/simple/tgui
keep_local_name = TRUE
assets = list(
"tgui.bundle.js" = 'tgui/public/tgui.bundle.js',
"tgui.bundle.css" = 'tgui/public/tgui.bundle.css',
"tgui.bundle.js" = file("tgui/public/tgui.bundle.js"),
"tgui.bundle.css" = file("tgui/public/tgui.bundle.css"),
)
/datum/asset/simple/tgui_panel
keep_local_name = TRUE
assets = list(
"tgui-panel.bundle.js" = 'tgui/public/tgui-panel.bundle.js',
"tgui-panel.bundle.css" = 'tgui/public/tgui-panel.bundle.css',
"tgui-panel.bundle.js" = file("tgui/public/tgui-panel.bundle.js"),
"tgui-panel.bundle.css" = file("tgui/public/tgui-panel.bundle.css"),
)
/datum/asset/simple/headers
@@ -149,10 +149,12 @@
/datum/asset/simple/namespaced/tgfont
assets = list(
"tgfont.eot" = 'tgui/packages/tgfont/dist/tgfont.eot',
"tgfont.woff2" = 'tgui/packages/tgfont/dist/tgfont.woff2',
"tgfont.eot" = file("tgui/packages/tgfont/dist/tgfont.eot"),
"tgfont.woff2" = file("tgui/packages/tgfont/dist/tgfont.woff2"),
)
parents = list(
"tgfont.css" = file("tgui/packages/tgfont/dist/tgfont.css"),
)
parents = list("tgfont.css" = 'tgui/packages/tgfont/dist/tgfont.css')
/datum/asset/spritesheet/chat
name = "chat"

View File

@@ -1 +1,2 @@
@call powershell.exe -NoLogo -ExecutionPolicy Bypass -File "%~dp0\python_.ps1" %*
@echo off
call powershell.exe -NoLogo -ExecutionPolicy Bypass -File "%~dp0\python_.ps1" %*

View File

@@ -1 +1,2 @@
@"%~dp0\..\bootstrap\node" "%~dp0\build.js"
@echo off
"%~dp0\..\bootstrap\node.bat" "%~dp0\build.js" %*

View File

@@ -1,225 +1,146 @@
#!/usr/bin/env node
/**
* @file
* @copyright 2020 Aleksej Komarov
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
// Change working directory to project root
process.chdir(require('path').resolve(__dirname, '../../'));
process.chdir(require("path").resolve(__dirname, "../../"));
// Validate NodeJS version
const NODE_VERSION = parseInt(process.versions.node.match(/(\d+)/)[1]);
const NODE_VERSION_TARGET = parseInt(require('fs')
.readFileSync('dependencies.sh', 'utf-8')
.match(/NODE_VERSION=(\d+)/)[1]);
const NODE_VERSION_TARGET = parseInt(
require("fs")
.readFileSync("dependencies.sh", "utf-8")
.match(/NODE_VERSION=(\d+)/)[1]
);
if (NODE_VERSION < NODE_VERSION_TARGET) {
console.error('Your current Node.js version is out of date.');
console.error('You have two options:');
console.error(' a) Go to https://nodejs.org/ and install the latest LTS release of Node.js');
console.error(' b) Uninstall Node.js (our build system automatically downloads one)');
console.error("Your current Node.js version is out of date.");
console.error("You have two options:");
console.error(
" a) Go to https://nodejs.org/ and install the latest LTS release of Node.js"
);
console.error(
" b) Uninstall Node.js (our build system automatically downloads one)"
);
process.exit(1);
}
const STANDARD_BUILD = "Standard Build"
const TGS_BUILD = "TGS Build"
const ALL_MAPS_BUILD = "CI All Maps Build"
const TEST_RUN_BUILD = "CI Integration Tests Build"
const NO_DM_BUILD = "Except DM Build"
let BUILD_MODE = STANDARD_BUILD;
if (process.env.CBT_BUILD_MODE) {
switch (process.env.CBT_BUILD_MODE) {
case "ALL_MAPS":
BUILD_MODE = ALL_MAPS_BUILD
break;
case "TEST_RUN":
BUILD_MODE = TEST_RUN_BUILD
break;
case "TGS":
BUILD_MODE = TGS_BUILD
break;
case "NO_DM":
BUILD_MODE = NO_DM_BUILD
break;
default:
BUILD_MODE = process.env.CBT_BUILD_MODE
break;
}
}
console.log(`Starting CBT in ${BUILD_MODE} mode.`)
const DME_NAME = 'tgstation'
// Main
// --------------------------------------------------------
const { resolveGlob, stat } = require('./cbt/fs');
const { exec } = require('./cbt/process');
const { Task, runTasks } = require('./cbt/task');
const { regQuery } = require('./cbt/winreg');
const fs = require('fs');
const fs = require("fs");
const Juke = require("./juke");
const { yarn } = require("./cbt/yarn");
const { dm } = require("./cbt/dm");
const yarn = args => {
const yarnPath = resolveGlob('./tgui/.yarn/releases/yarn-*.cjs')[0]
.replace('/tgui/', '/');
return exec('node', [yarnPath, ...args], {
cwd: './tgui',
});
};
const DME_NAME = "tgstation";
/** Installs all tgui dependencies */
const taskYarn = new Task('yarn')
// The following dependencies skip what could be considered an important
// step in Yarn: it verifies the integrity of cache. With this setup, if
// cache ever becomes corrupted, your only option is to clean build.
.depends('tgui/.yarn/+(cache|releases|plugins|sdks)/**/*')
.depends('tgui/**/package.json')
.depends('tgui/yarn.lock')
// Phony target (automatically created at the end of the task)
.provides('tgui/.yarn/install-target')
.build(() => yarn(['install']));
const YarnTarget = Juke.createTarget({
name: "yarn",
inputs: [
"tgui/.yarn/+(cache|releases|plugins|sdks)/**/*",
"tgui/**/package.json",
"tgui/yarn.lock",
],
outputs: ["tgui/.yarn/install-target"],
executes: () => yarn("install"),
});
/** Builds svg fonts */
const taskTgfont = new Task('tgfont')
.depends('tgui/.yarn/install-target')
.depends('tgui/packages/tgfont/**/*.+(js|cjs|svg)')
.depends('tgui/packages/tgfont/package.json')
.provides('tgui/packages/tgfont/dist/tgfont.css')
.provides('tgui/packages/tgfont/dist/tgfont.eot')
.provides('tgui/packages/tgfont/dist/tgfont.woff2')
.build(() => yarn(['workspace', 'tgfont', 'build']));
const TgFontTarget = Juke.createTarget({
name: "tgfont",
dependsOn: [YarnTarget],
inputs: [
"tgui/.yarn/install-target",
"tgui/packages/tgfont/**/*.+(js|cjs|svg)",
"tgui/packages/tgfont/package.json",
],
outputs: [
"tgui/packages/tgfont/dist/tgfont.css",
"tgui/packages/tgfont/dist/tgfont.eot",
"tgui/packages/tgfont/dist/tgfont.woff2",
],
executes: () => yarn("workspace", "tgfont", "build"),
});
/** Builds tgui */
const taskTgui = new Task('tgui')
.depends('tgui/.yarn/install-target')
.depends('tgui/webpack.config.js')
.depends('tgui/**/package.json')
.depends('tgui/packages/**/*.+(js|cjs|ts|tsx|scss)')
.provides('tgui/public/tgui.bundle.css')
.provides('tgui/public/tgui.bundle.js')
.provides('tgui/public/tgui-common.bundle.js')
.provides('tgui/public/tgui-panel.bundle.css')
.provides('tgui/public/tgui-panel.bundle.js')
.build(async () => {
await yarn(['run', 'webpack-cli', '--mode=production']);
});
const TguiTarget = Juke.createTarget({
name: "tgui",
dependsOn: [YarnTarget],
inputs: [
"tgui/.yarn/install-target",
"tgui/webpack.config.js",
"tgui/**/package.json",
"tgui/packages/**/*.+(js|cjs|ts|tsx|scss)",
],
outputs: [
"tgui/public/tgui.bundle.css",
"tgui/public/tgui.bundle.js",
"tgui/public/tgui-common.bundle.js",
"tgui/public/tgui-panel.bundle.css",
"tgui/public/tgui-panel.bundle.js",
],
executes: () => yarn("run", "webpack-cli", "--mode=production"),
});
const DefineParameter = Juke.createParameter({
type: "string[]",
name: "define",
alias: "D",
});
const DmTarget = Juke.createTarget({
name: "dm",
inputs: [
"_maps/map_files/generic/**",
"code/**",
"goon/**",
"html/**",
"icons/**",
"interface/**",
"modular_skyrat/**", //SKYRAT EDIT ADDITION -- check modular_skyrat too pls, build.js
`${DME_NAME}.dme`,
],
outputs: [`${DME_NAME}.dmb`, `${DME_NAME}.rsc`],
parameters: [DefineParameter],
executes: async ({ get }) => {
const defines = get(DefineParameter);
if (defines.length > 0) {
Juke.logger.info("Using defines:", defines.join(", "));
}
await dm(`${DME_NAME}.dme`, {
defines: ["CBT", ...defines],
});
},
});
const DefaultTarget = Juke.createTarget({
name: "default",
dependsOn: [TguiTarget, TgFontTarget, DmTarget],
});
/**
* Prepends the defines to the .dme.
* Does not clean them up, as this is intended for TGS which
* clones new copies anyway.
*/
const taskPrependDefines = (...defines) => new Task('prepend-defines')
.build(async () => {
const dmeContents = fs.readFileSync(`${DME_NAME}.dme`);
const textToWrite = defines.map(define => `#define ${define}\n`);
fs.writeFileSync(`${DME_NAME}.dme`, `${textToWrite}\n${dmeContents}`);
});
const prependDefines = (...defines) => {
const dmeContents = fs.readFileSync(`${DME_NAME}.dme`);
const textToWrite = defines.map((define) => `#define ${define}\n`);
fs.writeFileSync(`${DME_NAME}.dme`, `${textToWrite}\n${dmeContents}`);
};
const taskDm = (...injectedDefines) => new Task('dm')
.depends('_maps/map_files/generic/**')
.depends('code/**')
.depends('goon/**')
.depends('html/**')
.depends('icons/**')
.depends('interface/**')
.depends('modular_skyrat/**') // SKYRAT EDIT ADDITION -- check modular_skyrat too pls, build.js
.depends('tgui/public/tgui.html')
.depends('tgui/public/*.bundle.*')
.depends(`${DME_NAME}.dme`)
.provides(`${DME_NAME}.dmb`)
.provides(`${DME_NAME}.rsc`)
.build(async () => {
const dmPath = await (async () => {
// Search in array of paths
const paths = [
...((process.env.DM_EXE && process.env.DM_EXE.split(',')) || []),
'C:\\Program Files\\BYOND\\bin\\dm.exe',
'C:\\Program Files (x86)\\BYOND\\bin\\dm.exe',
['reg', 'HKLM\\Software\\Dantom\\BYOND', 'installpath'],
['reg', 'HKLM\\SOFTWARE\\WOW6432Node\\Dantom\\BYOND', 'installpath'],
];
const isFile = path => {
try {
const fstat = stat(path);
return fstat && fstat.isFile();
}
catch (err) {}
return false;
};
for (let path of paths) {
// Resolve a registry key
if (Array.isArray(path)) {
const [type, ...args] = path;
path = await regQuery(...args);
}
if (!path) {
continue;
}
// Check if path exists
if (isFile(path)) {
return path;
}
if (isFile(path + '/dm.exe')) {
return path + '/dm.exe';
}
if (isFile(path + '/bin/dm.exe')) {
return path + '/bin/dm.exe';
}
}
// Default paths
return (
process.platform === 'win32' && 'dm.exe'
|| 'DreamMaker'
);
})();
if (injectedDefines.length) {
const injectedContent = injectedDefines
.map(x => `#define ${x}\n`)
.join('')
// Create mdme file
fs.writeFileSync(`${DME_NAME}.mdme`, injectedContent)
// Add the actual dme content
const dme_content = fs.readFileSync(`${DME_NAME}.dme`)
fs.appendFileSync(`${DME_NAME}.mdme`, dme_content)
await exec(dmPath, [`${DME_NAME}.mdme`]);
// Rename dmb
fs.renameSync(`${DME_NAME}.mdme.dmb`, `${DME_NAME}.dmb`)
// Rename rsc
fs.renameSync(`${DME_NAME}.mdme.rsc`, `${DME_NAME}.rsc`)
// Remove mdme
fs.unlinkSync(`${DME_NAME}.mdme`)
}
else {
await exec(dmPath, [`${DME_NAME}.dme`]);
}
});
const TgsTarget = Juke.createTarget({
name: "tgs",
dependsOn: [TguiTarget, TgFontTarget],
executes: async () => {
Juke.logger.info("Prepending TGS define");
prependDefines("TGS");
},
});
// Frontend
let tasksToRun = [
taskYarn,
taskTgfont,
taskTgui,
];
switch (BUILD_MODE) {
case STANDARD_BUILD:
tasksToRun.push(taskDm('CBT'));
break;
case TGS_BUILD:
tasksToRun.push(taskPrependDefines('TGS'));
break;
case ALL_MAPS_BUILD:
tasksToRun.push(taskDm('CBT','CIBUILDING','CITESTING','ALL_MAPS'));
break;
case TEST_RUN_BUILD:
tasksToRun.push(taskDm('CBT','CIBUILDING'));
break;
case NO_DM_BUILD:
break;
default:
console.error(`Unknown build mode : ${BUILD_MODE}`)
break;
}
runTasks(tasksToRun);
Juke.setup({
default: process.env.CBT_BUILD_MODE === "TGS" ? TgsTarget : DefaultTarget,
}).then((code) => {
process.exit(code);
});

84
tools/build/cbt/dm.js Normal file
View File

@@ -0,0 +1,84 @@
const { exec } = require('../juke');
const { stat } = require('./fs');
const { regQuery } = require('./winreg');
const fs = require('fs');
/**
* Cached path to DM compiler
*/
let dmPath;
/**
* DM compiler
*
* @param {string} dmeFile
* @param {{ defines?: string[] }} options
*/
const dm = async (dmeFile, options = {}) => {
if (!dmPath) {
dmPath = await (async () => {
// Search in array of paths
const paths = [
...((process.env.DM_EXE && process.env.DM_EXE.split(',')) || []),
'C:\\Program Files\\BYOND\\bin\\dm.exe',
'C:\\Program Files (x86)\\BYOND\\bin\\dm.exe',
['reg', 'HKLM\\Software\\Dantom\\BYOND', 'installpath'],
['reg', 'HKLM\\SOFTWARE\\WOW6432Node\\Dantom\\BYOND', 'installpath'],
];
const isFile = path => {
try {
const fstat = stat(path);
return fstat && fstat.isFile();
}
catch (err) {}
return false;
};
for (let path of paths) {
// Resolve a registry key
if (Array.isArray(path)) {
const [type, ...args] = path;
path = await regQuery(...args);
}
if (!path) {
continue;
}
// Check if path exists
if (isFile(path)) {
return path;
}
if (isFile(path + '/dm.exe')) {
return path + '/dm.exe';
}
if (isFile(path + '/bin/dm.exe')) {
return path + '/bin/dm.exe';
}
}
// Default paths
return (
process.platform === 'win32' && 'dm.exe'
|| 'DreamMaker'
);
})();
}
const { defines } = options;
const dmeBaseName = dmeFile.replace(/\.dme$/, '');
if (defines && defines.length > 0) {
const injectedContent = defines
.map(x => `#define ${x}\n`)
.join('');
fs.writeFileSync(`${dmeBaseName}.mdme`, injectedContent)
const dmeContent = fs.readFileSync(`${dmeBaseName}.dme`)
fs.appendFileSync(`${dmeBaseName}.mdme`, dmeContent)
await exec(dmPath, [`${dmeBaseName}.mdme`]);
fs.renameSync(`${dmeBaseName}.mdme.dmb`, `${dmeBaseName}.dmb`)
fs.renameSync(`${dmeBaseName}.mdme.rsc`, `${dmeBaseName}.rsc`)
fs.unlinkSync(`${dmeBaseName}.mdme`)
}
else {
await exec(dmPath, dmeFile);
}
};
module.exports = {
dm,
};

View File

@@ -5,93 +5,6 @@
*/
const fs = require('fs');
const glob = require('./glob');
class File {
constructor(path) {
this.path = path;
}
get stat() {
if (this._stat === undefined) {
this._stat = stat(this.path);
}
return this._stat;
}
exists() {
return this.stat !== null;
}
get mtime() {
return this.stat && this.stat.mtime;
}
touch() {
const time = new Date();
try {
fs.utimesSync(this.path, time, time);
}
catch (err) {
fs.closeSync(fs.openSync(this.path, 'w'));
}
}
}
class Glob {
constructor(path) {
this.path = path;
}
toFiles() {
const paths = glob.sync(this.path, {
strict: false,
silent: true,
});
return paths
.map(path => new File(path))
.filter(file => file.exists());
}
}
/**
* If true, source is newer than target.
* @param {File[]} sources
* @param {File[]} targets
*/
const compareFiles = (sources, targets) => {
let bestSource = null;
let bestTarget = null;
for (const file of sources) {
if (!bestSource || file.mtime > bestSource.mtime) {
bestSource = file;
}
}
for (const file of targets) {
if (!file.exists()) {
return `target '${file.path}' is missing`;
}
if (!bestTarget || file.mtime < bestTarget.mtime) {
bestTarget = file;
}
}
// Doesn't need rebuild if there is no source, but target exists.
if (!bestSource) {
if (bestTarget) {
return false;
}
return 'no known sources or targets';
}
// Always needs a rebuild if no targets were specified (e.g. due to GLOB).
if (!bestTarget) {
return 'no targets were specified';
}
// Needs rebuild if source is newer than target
if (bestSource.mtime > bestTarget.mtime) {
return `source '${bestSource.path}' is newer than target '${bestTarget.path}'`;
}
return false;
};
/**
* Returns file stats for the provided path, or null if file is
@@ -106,30 +19,6 @@ const stat = path => {
}
};
/**
* Resolves a glob pattern and returns files that are safe
* to call `stat` on.
*/
const resolveGlob = globPath => {
const unsafePaths = glob.sync(globPath, {
strict: false,
silent: true,
});
const safePaths = [];
for (let path of unsafePaths) {
try {
fs.statSync(path);
safePaths.push(path);
}
catch {}
}
return safePaths;
};
module.exports = {
File,
Glob,
compareFiles,
stat,
resolveGlob,
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,102 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
const { spawn } = require('child_process');
const { resolve: resolvePath } = require('path');
const { stat } = require('./fs');
/**
* @type {import('child_process').ChildProcessWithoutNullStreams}
*/
const children = new Set();
const killChildren = () => {
for (const child of children) {
child.kill('SIGTERM');
children.delete(child);
console.log('killed child process');
}
};
const trap = (signals, handler) => {
let readline;
if (process.platform === 'win32') {
readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout,
});
}
for (const signal of signals) {
const handleSignal = () => handler(signal);
if (signal === 'EXIT') {
process.on('exit', handleSignal);
continue;
}
if (readline) {
readline.on('SIG' + signal, handleSignal);
}
process.on('SIG' + signal, handleSignal);
}
};
trap(['EXIT', 'BREAK', 'HUP', 'INT', 'TERM'], signal => {
if (signal !== 'EXIT') {
console.log('Received', signal);
}
killChildren();
if (signal !== 'EXIT') {
process.exit(1);
}
});
const exceptionHandler = err => {
console.log(err);
killChildren();
process.exit(1);
};
process.on('unhandledRejection', exceptionHandler);
process.on('uncaughtException', exceptionHandler);
class ExitError extends Error {}
/**
* @param {string} executable
* @param {string[]} args
* @param {import('child_process').SpawnOptionsWithoutStdio} options
*/
const exec = (executable, args, options) => {
return new Promise((resolve, reject) => {
// If executable exists relative to the current directory,
// use that executable, otherwise spawn should fall back to
// running it from PATH.
if (stat(executable)) {
executable = resolvePath(executable);
}
const child = spawn(executable, args, options);
children.add(child);
child.stdout.pipe(process.stdout, { end: false });
child.stderr.pipe(process.stderr, { end: false });
child.stdin.end();
child.on('error', err => reject(err));
child.on('exit', code => {
children.delete(child);
if (code !== 0) {
const error = new ExitError('Process exited with code: ' + code);
error.code = code;
reject(error);
}
else {
resolve(code);
}
});
});
};
module.exports = {
exec,
ExitError,
};

View File

@@ -1,107 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
const { compareFiles, Glob, File } = require('./fs');
class Task {
constructor(name) {
this.name = name;
this.sources = [];
this.targets = [];
this.script = null;
}
depends(path) {
if (path.includes('*')) {
this.sources.push(new Glob(path));
}
else {
this.sources.push(new File(path));
}
return this;
}
provides(path) {
if (path.includes('*')) {
this.targets.push(new Glob(path));
}
else {
this.targets.push(new File(path));
}
return this;
}
build(script) {
this.script = script;
return this;
}
async run() {
/**
* @returns {File[]}
*/
const getFiles = files => files
.flatMap(file => {
if (file instanceof Glob) {
return file.toFiles();
}
if (file instanceof File) {
return file;
}
})
.filter(Boolean);
// Normalize all our dependencies by converting globs to files
const fileSources = getFiles(this.sources);
const fileTargets = getFiles(this.targets);
// Consider dependencies first, and skip the task if it
// doesn't need a rebuild.
let needsRebuild = 'no targets';
if (fileTargets.length > 0) {
needsRebuild = compareFiles(fileSources, fileTargets);
if (!needsRebuild) {
console.warn(` => Skipping '${this.name}' (up to date)`);
return;
}
}
if (!this.script) {
return;
}
if (process.env.DEBUG && needsRebuild) {
console.debug(` Reason: ${needsRebuild}`);
}
console.warn(` => Starting '${this.name}'`);
const startedAt = Date.now();
// Run the script
await this.script();
// Touch all targets so that they don't rebuild again
if (fileTargets.length > 0) {
for (const file of fileTargets) {
file.touch();
}
}
const time = ((Date.now() - startedAt) / 1000) + 's';
console.warn(` => Finished '${this.name}' in ${time}`);
}
}
const runTasks = async tasks => {
const startedAt = Date.now();
// Run all if none of the tasks were specified in command line
const runAll = !tasks.some(task => process.argv.includes(task.name));
for (const task of tasks) {
if (runAll || process.argv.includes(task.name)) {
await task.run();
}
}
const time = ((Date.now() - startedAt) / 1000) + 's';
console.log(` => Done in ${time}`);
process.exit();
};
module.exports = {
Task,
runTasks,
};

17
tools/build/cbt/yarn.js Normal file
View File

@@ -0,0 +1,17 @@
const { exec, resolveGlob } = require('../juke');
let yarnPath;
const yarn = (...args) => {
if (!yarnPath) {
yarnPath = resolveGlob('./tgui/.yarn/releases/yarn-*.cjs')[0]
.replace('/tgui/', '/');
}
return exec('node', [yarnPath, ...args], {
cwd: './tgui',
});
};
module.exports = {
yarn,
};

18
tools/build/juke/argparse.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
import { Parameter, ParameterMap } from './parameter';
declare type TaskArgs = [
/** Task name */
string,
/** Task arguments */
...string[]
];
/**
* Returns global flags and tasks, which is an array of this format:
* `[[taskName, ...taskArgs], ...]`
* @param args List of command line arguments
*/
export declare const prepareArgs: (args: string[]) => {
globalFlags: string[];
taskArgs: TaskArgs[];
};
export declare const parseArgs: (args: string[], parameters: Parameter[]) => ParameterMap;
export {};

6
tools/build/juke/exec.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
import { SpawnOptionsWithoutStdio } from 'child_process';
export declare class ExitError extends Error {
code: number | null;
signal: string | null;
}
export declare const exec: (executable: string, args?: string[], options?: SpawnOptionsWithoutStdio) => Promise<void>;

30
tools/build/juke/fs.d.ts vendored Normal file
View File

@@ -0,0 +1,30 @@
/// <reference types="node" />
import fs from 'fs';
export declare class File {
readonly path: string;
private _stat?;
constructor(path: string);
get stat(): fs.Stats | null;
exists(): boolean;
get mtime(): Date | null;
touch(): void;
}
export declare class Glob {
readonly path: string;
constructor(path: string);
toFiles(): File[];
}
/**
* If true, source is newer than target.
*/
export declare const compareFiles: (sources: File[], targets: File[]) => string | false;
/**
* Returns file stats for the provided path, or null if file is
* not accessible.
*/
export declare const stat: (path: string) => fs.Stats | null;
/**
* Resolves a glob pattern and returns files that are safe
* to call `stat` on.
*/
export declare const resolveGlob: (globPath: string) => string[];

23
tools/build/juke/index.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import chalk from 'chalk';
import glob from 'glob';
import { exec } from './exec';
import { logger } from './logger';
import { createParameter as _createParameter } from './parameter';
import { RunnerConfig } from './runner';
import { createTarget as _createTarget } from './target';
export { exec, chalk, glob, logger };
/**
* Configures Juke Build and starts executing targets.
*
* @param config Juke Build configuration.
* @returns Exit code of the whole runner process.
*/
export declare const setup: (config?: RunnerConfig) => Promise<number>;
export declare const createTarget: typeof _createTarget;
export declare const createParameter: typeof _createParameter;
export declare const sleep: (time: number) => Promise<unknown>;
/**
* Resolves a glob pattern and returns files that are safe
* to call `stat` on.
*/
export declare const resolveGlob: (globPath: string) => string[];

7170
tools/build/juke/index.js Normal file

File diff suppressed because it is too large Load Diff

8
tools/build/juke/logger.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
export declare const logger: {
log: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
action: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
info: (...args: unknown[]) => void;
debug: (...args: unknown[]) => void;
};

39
tools/build/juke/parameter.d.ts vendored Normal file
View File

@@ -0,0 +1,39 @@
export declare type ParameterType = (string | string[] | number | number[] | boolean | boolean[]);
export declare type ParameterStringType = ('string' | 'string[]' | 'number' | 'number[]' | 'boolean' | 'boolean[]');
declare type ParameterTypeByString<T extends ParameterStringType> = (T extends 'string' ? string : T extends 'string[]' ? string[] : T extends 'number' ? number : T extends 'number[]' ? number[] : T extends 'boolean' ? boolean : T extends 'boolean[]' ? boolean[] : never);
export declare type ParameterMap = Map<Parameter, unknown[]>;
declare type ParameterOptions<T extends ParameterStringType> = {
/**
* Parameter name, in "camelCase".
*/
readonly name: string;
/**
* Parameter type, one of:
* - `string`
* - `string[]`
* - `number`
* - `number[]`
* - `boolean`
* - `boolean[]`
*/
readonly type: T;
/**
* Short flag for use in CLI, can only be a single character.
*/
readonly alias?: string;
};
export declare const createParameter: <T extends ParameterStringType>(options: ParameterOptions<T>) => Parameter<ParameterTypeByString<T>>;
export declare class Parameter<T extends ParameterType = any> {
readonly name: string;
readonly type: ParameterStringType;
readonly alias?: string | undefined;
constructor(name: string, type: ParameterStringType, alias?: string | undefined);
isString(): T extends string | string[] ? true : false;
isNumber(): T extends number | number[] ? true : false;
isBoolean(): T extends boolean | boolean[] ? true : false;
isArray(): T extends Array<unknown> ? true : false;
toKebabCase(): string;
toConstCase(): string;
toCamelCase(): string;
}
export {};

38
tools/build/juke/runner.d.ts vendored Normal file
View File

@@ -0,0 +1,38 @@
/// <reference types="node" />
import EventEmitter from 'events';
import { Parameter, ParameterType } from './parameter';
import { Target } from './target';
export declare type RunnerConfig = {
targets?: Target[];
default?: Target;
parameters?: Parameter[];
};
export declare type ExecutionContext = {
/** Get parameter value. */
get: <T extends ParameterType>(parameter: Parameter<T>) => (T extends Array<unknown> ? T : T | null);
};
export declare const runner: {
defaultTarget?: Target | undefined;
targets: Target[];
parameters: Parameter[];
workers: Worker[];
configure(config: RunnerConfig): void;
start(): Promise<number>;
};
declare class Worker {
readonly target: Target;
readonly context: ExecutionContext;
dependencies: Set<Target>;
generator?: AsyncGenerator;
emitter: EventEmitter;
hasFailed: boolean;
constructor(target: Target, context: ExecutionContext);
resolveDependency(target: Target): void;
rejectDependency(target: Target): void;
start(): void;
onFinish(fn: () => void): void;
onFail(fn: () => void): void;
private debugLog;
private process;
}
export {};

50
tools/build/juke/target.d.ts vendored Normal file
View File

@@ -0,0 +1,50 @@
import { Parameter } from './parameter';
declare type BuildFn = (...args: any) => unknown;
export declare type Target = {
name: string;
dependsOn: Target[];
executes: BuildFn[];
inputs: string[];
outputs: string[];
parameters: Parameter[];
};
declare type TargetConfig = {
/**
* Target name. This parameter is required.
*/
name: string;
/**
* Dependencies for this target. They will be ran before executing this
* target, and may run in parallel.
*/
dependsOn?: Target[];
/**
* Function that is delegated to the execution engine for building this
* target. It is normally an async function, which accepts a single
* argument - execution context (contains `get` for interacting with
* parameters).
*
* @example
* executes: async ({ get }) => {
* console.log(get(Parameter));
* },
*/
executes?: BuildFn | BuildFn[];
/**
* Files that are consumed by this target.
*/
inputs?: string[];
/**
* Files that are produced by this target. Additionally, they are also
* touched every time target finishes executing in order to stop
* this target from re-running.
*/
outputs?: string[];
/**
* Parameters that are local to this task. Can be retrieved via `get`
* in the executor function.
*/
parameters?: Parameter[];
};
export declare const createTarget: (target: TargetConfig) => Target;
export {};

View File

@@ -13,7 +13,9 @@ mkdir -p \
$1/_maps \
$1/icons/runtime \
$1/sound/runtime \
$1/strings
$1/strings \
$1/tgui/public \
$1/tgui/packages/tgfont/dist
if [ -d ".git" ]; then
mkdir -p $1/.git/logs
@@ -25,6 +27,8 @@ cp -r _maps/* $1/_maps/
cp -r icons/runtime/* $1/icons/runtime/
cp -r sound/runtime/* $1/sound/runtime/
cp -r strings/* $1/strings/
cp -r tgui/public/* $1/tgui/public/
cp -r tgui/packages/tgfont/dist/* $1/tgui/packages/tgfont/dist/
#remove .dm files from _maps