mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-09 16:05:07 +00:00
[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:
8
.github/workflows/ci_suite.yml
vendored
8
.github/workflows/ci_suite.yml
vendored
@@ -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]')"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
@call "%~dp0\tools\build\build"
|
||||
@pause
|
||||
@echo off
|
||||
call "%~dp0\tools\build\build.bat" %*
|
||||
pause
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" %*
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
@"%~dp0\..\bootstrap\node" "%~dp0\build.js"
|
||||
@echo off
|
||||
"%~dp0\..\bootstrap\node.bat" "%~dp0\build.js" %*
|
||||
|
||||
@@ -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
84
tools/build/cbt/dm.js
Normal 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,
|
||||
};
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
@@ -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
17
tools/build/cbt/yarn.js
Normal 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
18
tools/build/juke/argparse.d.ts
vendored
Normal 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
6
tools/build/juke/exec.d.ts
vendored
Normal 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
30
tools/build/juke/fs.d.ts
vendored
Normal 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
23
tools/build/juke/index.d.ts
vendored
Normal 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
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
8
tools/build/juke/logger.d.ts
vendored
Normal 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
39
tools/build/juke/parameter.d.ts
vendored
Normal 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
38
tools/build/juke/runner.d.ts
vendored
Normal 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
50
tools/build/juke/target.d.ts
vendored
Normal 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 {};
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user