cbt => juke

This commit is contained in:
LetterN
2021-09-08 09:13:14 +08:00
parent e203abb25c
commit 44b9e14391
21 changed files with 5822 additions and 3973 deletions

76
.gitignore vendored
View File

@@ -7,14 +7,16 @@
#Ignore byond config folder.
/cfg/**/*
#Ignore rust-g and auxmos libraries which are compiled with scripts
*.so
/tools/build/binaries/**/*
# Ignore compiled linux libs in the root folder, e.g. librust_g.so
/*.so
#Ignore compiled files and other files generated during compilation.
*.mdme
*.mdme.*
*.dmb
*.rsc
*.m.dme
*.test.dme
*.lk
*.int
*.backup
@@ -53,27 +55,6 @@ __pycache__/
*.py[cod]
*$py.class
# C extensions
#*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
@@ -81,8 +62,7 @@ var/
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
pip-*.txt
# Unit test / coverage reports
htmlcov/
@@ -92,7 +72,6 @@ htmlcov/
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
@@ -101,10 +80,6 @@ coverage.xml
# Django stuff:
*.log
local_settings.py
# Flask instance folder
instance/
# Scrapy stuff:
.scrapy
@@ -112,9 +87,6 @@ instance/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
@@ -127,10 +99,6 @@ celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# IntelliJ IDEA / PyCharm (with plugin)
.idea
@@ -153,12 +121,6 @@ Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
#*.cab
#*.msi
#*.msm
#*.msp
# Windows shortcuts
*.lnk
@@ -199,11 +161,10 @@ Temporary Items
#Visual studio stuff
*.vscode/*
!/.vscode/extensions.json
tools/MapAtmosFixer/MapAtmosFixer/obj/*
tools/MapAtmosFixer/MapAtmosFixer/bin/*
tools/CreditsTool/bin/*
tools/CreditsTool/obj/*
/tools/MapAtmosFixer/MapAtmosFixer/obj/*
/tools/MapAtmosFixer/MapAtmosFixer/bin/*
/tools/CreditsTool/bin/*
/tools/CreditsTool/obj/*
#GitHub Atom
.atom-build.json
@@ -228,13 +189,10 @@ tools/CreditsTool/obj/*
!/config/title_screens/images/exclude
#Linux docker
tools/LinuxOneShot/SetupProgram/obj/*
tools/LinuxOneShot/SetupProgram/bin/*
tools/LinuxOneShot/SetupProgram/.vs
tools/LinuxOneShot/Database
tools/LinuxOneShot/TGS_Config
tools/LinuxOneShot/TGS_Instances
tools/LinuxOneShot/TGS_Logs
# Common build tooling
!/tools/build
/tools/LinuxOneShot/SetupProgram/obj/*
/tools/LinuxOneShot/SetupProgram/bin/*
/tools/LinuxOneShot/SetupProgram/.vs
/tools/LinuxOneShot/Database
/tools/LinuxOneShot/TGS_Config
/tools/LinuxOneShot/TGS_Instances
/tools/LinuxOneShot/TGS_Logs

View File

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

3
CLEAN.bat Normal file
View File

@@ -0,0 +1,3 @@
@echo off
call "%~dp0\tools\build\build.bat" dist-clean
pause

3
RUN_SERVER.bat Normal file
View File

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

View File

@@ -13,6 +13,14 @@ This build script is the recommended way to compile the game, including not only
The script will skip build steps whose inputs have not changed since the last run.
## Getting list of available targets
You can get a list of all targets that you can build by running the following command:
```
tools/build/build --help
```
## Dependencies
- On Windows, `BUILD.bat` will automatically install a private (vendored) copy of Node.
@@ -22,3 +30,5 @@ The script will skip build steps whose inputs have not changed since the last ru
## Why?
We used to include compiled versions of the tgui JavaScript code in the Git repository so that the project could be compiled using BYOND only. These pre-compiled files tended to have merge conflicts for no good reason. Using a build script lets us avoid this problem, while keeping builds convenient for people who are not modifying tgui.
This build script is based on [Juke Build](https://github.com/stylemistake/juke-build) - please follow the link and read the documentation for the project to understand how it works and how to contribute to this build script.

View File

@@ -1 +0,0 @@
This directory is used to store temporary files to create binaries on linux

View File

@@ -1,6 +1,4 @@
#!/bin/sh
#Build TGUI
set -e
cd "$(dirname "$0")"
exec ../bootstrap/node build.js "$@"

View File

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

View File

@@ -1,225 +1,293 @@
#!/usr/bin/env node
/**
* Build script for /tg/station 13 codebase.
*
* This script uses Juke Build, read the docs here:
* https://github.com/stylemistake/juke-build
*
* @file
* @copyright 2020 Aleksej Komarov
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
// Change working directory to project root
process.chdir(require('path').resolve(__dirname, '../../'));
import fs from 'fs';
import { DreamDaemon, DreamMaker } from './lib/byond.js';
import { yarn } from './lib/yarn.js';
import Juke from './juke/index.js';
// 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]);
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)');
process.exit(1);
}
Juke.chdir('../..', import.meta.url);
Juke.setup({ file: import.meta.url }).then((code) => process.exit(code));
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"
const DME_NAME = 'tgstation';
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;
export const DefineParameter = new Juke.Parameter({
type: 'string[]',
alias: 'D',
});
export const PortParameter = new Juke.Parameter({
type: 'string',
alias: 'p',
});
export const CiParameter = new Juke.Parameter({
type: 'boolean',
});
export const DmMapsIncludeTarget = new Juke.Target({
executes: async () => {
const folders = [
...Juke.glob('_maps/RandomRuins/**/*.dmm'),
...Juke.glob('_maps/RandomZLevels/**/*.dmm'),
...Juke.glob('_maps/shuttles/**/*.dmm'),
...Juke.glob('_maps/templates/**/*.dmm'),
];
const content = folders
.map((file) => file.replace('_maps/', ''))
.map((file) => `#include "${file}"`)
.join('\n') + '\n';
fs.writeFileSync('_maps/templates.dm', content);
},
});
export const DmTarget = new Juke.Target({
dependsOn: ({ get }) => [
get(DefineParameter).includes('ALL_MAPS') && DmMapsIncludeTarget,
],
inputs: [
'_maps/map_files/generic/**',
'code/**',
'goon/**',
'html/**',
'icons/**',
'interface/**',
`${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(', '));
}
}
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 yarn = args => {
const yarnPath = resolveGlob('./tgui/.yarn/releases/yarn-*.cjs')[0]
.replace('/tgui/', '/');
return exec('node', [yarnPath, ...args], {
cwd: './tgui',
await DreamMaker(`${DME_NAME}.dme`, {
defines: ['CBT', ...defines],
});
};
},
});
/** 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']));
/** 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']));
/** 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']);
export const DmTestTarget = new Juke.Target({
dependsOn: ({ get }) => [
get(DefineParameter).includes('ALL_MAPS') && DmMapsIncludeTarget,
],
executes: async ({ get }) => {
const defines = get(DefineParameter);
if (defines.length > 0) {
Juke.logger.info('Using defines:', defines.join(', '));
}
fs.copyFileSync(`${DME_NAME}.dme`, `${DME_NAME}.test.dme`);
await DreamMaker(`${DME_NAME}.test.dme`, {
defines: ['CBT', 'CIBUILDING', ...defines],
});
Juke.rm('data/logs/ci', { recursive: true });
await DreamDaemon(
`${DME_NAME}.test.dmb`,
'-close', '-trusted', '-verbose',
'-params', 'log-directory=ci'
);
Juke.rm('*.test.*');
try {
const cleanRun = fs.readFileSync('data/logs/ci/clean_run.lk', 'utf-8');
console.log(cleanRun);
}
catch (err) {
Juke.logger.error('Test run was not clean, exiting');
throw new Juke.ExitCode(1);
}
},
});
export const YarnTarget = new Juke.Target({
inputs: [
'tgui/.yarn/+(cache|releases|plugins|sdks)/**/*',
'tgui/**/package.json',
'tgui/yarn.lock',
],
outputs: [
'tgui/.yarn/install-target',
],
executes: async () => {
await yarn('install');
},
});
export const TgFontTarget = new Juke.Target({
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: async () => {
await yarn('workspace', 'tgfont', 'build');
},
});
export const TguiTarget = new Juke.Target({
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-panel.bundle.css',
'tgui/public/tgui-panel.bundle.js',
],
executes: async () => {
await yarn('webpack-cli', '--mode=production');
},
});
export const TguiEslintTarget = new Juke.Target({
dependsOn: [YarnTarget],
executes: async ({ args }) => {
await yarn(
'eslint', 'packages',
'--fix', '--ext', '.js,.cjs,.ts,.tsx',
...args
);
},
});
export const TguiTscTarget = new Juke.Target({
dependsOn: [YarnTarget],
executes: async () => {
await yarn('tsc');
},
});
export const TguiTestTarget = new Juke.Target({
dependsOn: [YarnTarget],
executes: async ({ args }) => {
await yarn('jest', ...args);
},
});
export const TguiLintTarget = new Juke.Target({
dependsOn: [YarnTarget, TguiEslintTarget, TguiTscTarget, TguiTestTarget],
});
export const TguiDevTarget = new Juke.Target({
dependsOn: [YarnTarget],
executes: async ({ args }) => {
await yarn('node', 'packages/tgui-dev-server/index.js', ...args);
},
});
export const TguiAnalyzeTarget = new Juke.Target({
dependsOn: [YarnTarget],
executes: async () => {
await yarn('webpack-cli', '--mode=production', '--analyze');
},
});
export const TestTarget = new Juke.Target({
dependsOn: [DmTestTarget, TguiTestTarget],
});
export const LintTarget = new Juke.Target({
dependsOn: [TguiLintTarget],
});
export const BuildTarget = new Juke.Target({
dependsOn: [TguiTarget, TgFontTarget, DmTarget],
});
export const ServerTarget = new Juke.Target({
dependsOn: [BuildTarget],
executes: async ({ get }) => {
const port = get(PortParameter) || '1337';
await DreamDaemon(`${DME_NAME}.dmb`, port, '-trusted');
},
});
export const AllTarget = new Juke.Target({
dependsOn: [TestTarget, LintTarget, BuildTarget],
});
/**
* Removes the immediate build junk to produce clean builds.
*/
export const CleanTarget = new Juke.Target({
executes: async () => {
Juke.rm('*.dmb');
Juke.rm('*.rsc');
Juke.rm('*.mdme');
Juke.rm('*.mdme*');
Juke.rm('*.m.*');
Juke.rm('_maps/templates.dm');
Juke.rm('tgui/public/.tmp', { recursive: true });
Juke.rm('tgui/public/*.map');
Juke.rm('tgui/public/*.chunk.*');
Juke.rm('tgui/public/*.bundle.*');
Juke.rm('tgui/public/*.hot-update.*');
Juke.rm('tgui/packages/tgfont/dist', { recursive: true });
Juke.rm('tgui/.yarn/cache', { recursive: true });
Juke.rm('tgui/.yarn/unplugged', { recursive: true });
Juke.rm('tgui/.yarn/webpack', { recursive: true });
Juke.rm('tgui/.yarn/build-state.yml');
Juke.rm('tgui/.yarn/install-state.gz');
Juke.rm('tgui/.yarn/install-target');
Juke.rm('tgui/.pnp.*');
},
});
/**
* Removes more junk at expense of much slower initial builds.
*/
export const DistCleanTarget = new Juke.Target({
dependsOn: [CleanTarget],
executes: async () => {
Juke.logger.info('Cleaning up data/logs');
Juke.rm('data/logs', { recursive: true });
Juke.logger.info('Cleaning up bootstrap cache');
Juke.rm('tools/bootstrap/.cache', { recursive: true });
Juke.logger.info('Cleaning up global yarn cache');
await yarn('cache', 'clean', '--all');
},
});
/**
* 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 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(process.platform === 'win32' ? 'auxmos.*' : 'libauxmos.*')
.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`]);
}
});
export const TgsTarget = new Juke.Target({
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;
}
const TGS_MODE = process.env.CBT_BUILD_MODE === 'TGS';
runTasks(tasksToRun);
export default TGS_MODE ? TgsTarget : BuildTarget;

View File

@@ -1,75 +0,0 @@
#!/bin/sh
#Detect OS and use corresponding package manager for dependencies. Currently only works for arch, debian/ubuntu, and RHEL/fedora/CentOS
if [[ -f '/etc/arch-release' ]]; then
echo -ne '\n y' | sudo pacman --needed -Sy base-devel git curl nodejs unzip
fi
if [[ -f '/etc/debian version' ]]; then
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y build-essential git curl lib32z1 pkg-config libssl-dev:i386 libssl-dev nodejs unzip g++-multilib libc6-i386 libstdc++6:i386
fi
if [[ -f '/etc/centos-release' ]] || [[ -f '/etc/fedora-release' ]]; then #DNF should work for both of these
sudo dnf --refresh install make automake gcc gcc-c++ kernel-devel git curl unzip glibc-devel.i686 openssl-devel.i686 libgcc.i686 libstdc++-devel.i686
fi
cd binaries
#Install rust if not present
if ! [ -x "$has_cargo" ]; then
echo "Installing rust..."
curl https://sh.rustup.rs -sSf | sh -s -- -y
. ~/.profile
fi
#Download/update rust-g repo
if [ ! -d "rust-g" ]; then
echo "Cloning rust-g..."
git clone https://github.com/tgstation/rust-g
cd rust-g
~/.cargo/bin/rustup target add i686-unknown-linux-gnu
else
echo "Fetching rust-g..."
cd rust-g
git fetch
~/.cargo/bin/rustup target add i686-unknown-linux-gnu
fi
#Compile and move rust-g binary to repo root
echo "Deploying rust-g..."
git checkout "$RUST_G_VERSION"
env PKG_CONFIG_ALLOW_CROSS=1 ~/.cargo/bin/cargo build --release --target=i686-unknown-linux-gnu
mv target/i686-unknown-linux-gnu/release/librust_g.so ../../../../librust_g.so
cd ..
#Download/update auxmos repo
if [ ! -d "auxmos" ]; then
echo "Cloning auxmos..."
git clone https://github.com/Putnam3145/auxmos
cd auxmos
~/.cargo/bin/rustup target add i686-unknown-linux-gnu
else
echo "Fetching auxmos..."
cd auxmos
git fetch
~/.cargo/bin/rustup target add i686-unknown-linux-gnu
fi
#Compile and move auxmos binary to repo root
echo "Deploying auxmos..."
git checkout "$AUXMOS_VERSION"
env PKG_CONFIG_ALLOW_CROSS=1 ~/.cargo/bin/cargo rustc --release --target=i686-unknown-linux-gnu --features all_reaction_hooks,explosive_decompression -- -C target-cpu=native
mv target/i686-unknown-linux-gnu/release/libauxmos.so ../../../../libauxmos.so
cd ../..
#Install BYOND
cd ../..
./tools/ci/install_byond.sh
source $HOME/BYOND/byond/bin/byondsetup
cd tools/build
#Build TGUI
set -e
cd "$(dirname "$0")"
exec ../bootstrap/node build.js "$@"

View File

@@ -1,135 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
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
* not accessible.
*/
const stat = path => {
try {
return fs.statSync(path);
}
catch {
return null;
}
};
/**
* 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,
};

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

@@ -0,0 +1,248 @@
// Generated by dts-bundle-generator v5.9.0
/// <reference types="node" />
import _chalk from 'chalk';
import { SpawnOptionsWithoutStdio } from 'child_process';
import EventEmitter from 'events';
/**
* Change the current working directory of the Node.js process.
*
* Second argument is a file (or directory), relative to which chdir will be
* performed. This is usually `import.meta.url`.
*/
export declare const chdir: (directory: string, relativeTo?: string | undefined) => void;
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;
};
export declare type ParameterType = (string | string[] | number | number[] | boolean | boolean[]);
export declare type StringType = ("string" | "string[]" | "number" | "number[]" | "boolean" | "boolean[]");
export declare type TypeByString<T extends StringType> = (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 ParameterConfig<T extends StringType> = {
/**
* Parameter name, as it would be used in CLI.
*/
name?: string;
/**
* Parameter type, one of:
* - `string`
* - `string[]`
* - `number`
* - `number[]`
* - `boolean`
* - `boolean[]`
*/
type: T;
/**
* Short flag for use in CLI, can only be a single character.
*/
alias?: string;
};
export interface Parameter<T extends ParameterType = ParameterType> {
type: StringType;
name?: string;
alias?: string;
__internalType?: T;
isString(): this is Parameter<string | string[]>;
isNumber(): this is Parameter<number | number[]>;
isBoolean(): this is Parameter<boolean | boolean[]>;
isArray(): this is Parameter<string[] | number[] | boolean[]>;
toKebabCase(): string | undefined;
toConstCase(): string | undefined;
toCamelCase(): string | undefined;
}
export declare type ParameterCtor = {
new <T extends StringType>(config: ParameterConfig<T>): Parameter<TypeByString<T>>;
};
export declare const Parameter: ParameterCtor;
export declare type ParameterCreator = <T extends StringType>(config: ParameterConfig<T>) => Parameter<TypeByString<T>>;
export declare const createParameter: ParameterCreator;
export declare type ExecutionContext = {
/** Get parameter value. */
get: <T extends ParameterType>(parameter: Parameter<T>) => (T extends Array<unknown> ? T : T | null);
args: string[];
};
export declare type BooleanLike = boolean | null | undefined;
export declare type WithExecutionContext<R> = (context: ExecutionContext) => R | Promise<R>;
export declare type WithOptionalExecutionContext<R> = R | WithExecutionContext<R>;
export declare type DependsOn = WithOptionalExecutionContext<(Target | BooleanLike)[]>;
export declare type ExecutesFn = WithExecutionContext<unknown>;
export declare type OnlyWhenFn = WithExecutionContext<BooleanLike>;
export declare type FileIo = WithOptionalExecutionContext<(string | BooleanLike)[]>;
export 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?: DependsOn;
/**
* 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?: ExecutesFn;
/**
* Files that are consumed by this target.
*/
inputs?: FileIo;
/**
* 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?: FileIo;
/**
* Parameters that are local to this task. Can be retrieved via `get`
* in the executor function.
*/
parameters?: Parameter[];
/**
* Target will run only when this function returns true. It accepts a
* single argument - execution context.
*/
onlyWhen?: OnlyWhenFn;
};
export declare class Target {
name?: string;
dependsOn: DependsOn;
executes?: ExecutesFn;
inputs: FileIo;
outputs: FileIo;
parameters: Parameter[];
onlyWhen?: OnlyWhenFn;
constructor(target: TargetConfig);
}
export declare type TargetCreator = (target: TargetConfig) => Target;
export declare const createTarget: TargetCreator;
export declare type RunnerConfig = {
targets?: Target[];
default?: Target;
parameters?: Parameter[];
singleTarget?: boolean;
};
export declare const runner: {
config: RunnerConfig;
targets: Target[];
parameters: Parameter[];
workers: Worker[];
configure(config: RunnerConfig): void;
start(): Promise<number>;
};
declare class Worker {
readonly target: Target;
readonly context: ExecutionContext;
readonly dependsOn: Target[];
dependencies: Set<Target>;
generator?: AsyncGenerator;
emitter: EventEmitter;
hasFailed: boolean;
constructor(target: Target, context: ExecutionContext, dependsOn: Target[]);
resolveDependency(target: Target): void;
rejectDependency(target: Target): void;
start(): void;
onFinish(fn: () => void): void;
onFail(fn: () => void): void;
private debugLog;
private process;
}
export declare class ExitCode extends Error {
code: number | null;
signal: string | null;
constructor(code: number | null, signal?: string | null);
}
export declare type ExecOptions = SpawnOptionsWithoutStdio & {
/**
* If `true`, this exec call will not pipe its output to stdio.
* @default false
*/
silent?: boolean;
/**
* Throw an exception on non-zero exit code.
* @default true
*/
throw?: boolean;
};
export declare type ExecReturn = {
/** Exit code of the program. */
code: number | null;
/** Signal received by the program which caused it to exit. */
signal: NodeJS.Signals | null;
/** Output collected from `stdout` */
stdout: string;
/** Output collected from `stderr` */
stderr: string;
/** A combined output collected from `stdout` and `stderr`. */
combined: string;
};
export declare const exec: (executable: string, args?: string[], options?: ExecOptions) => Promise<ExecReturn>;
/**
* Unix style pathname pattern expansion.
*
* Perform a search matching a specified pattern according to the rules of
* the `glob` npm package. Path can be either absolute or relative, and can
* contain shell-style wildcards. Broken symlinks are included in the results
* (as in the shell). Whether or not the results are sorted depends on the
* file system.
*
* @returns A possibly empty list of file paths.
*/
export declare const glob: (globPath: string) => string[];
export declare type RmOptions = {
/**
* If true, perform a recursive directory removal.
*/
recursive?: boolean;
/**
* If true, exceptions will be ignored if file or directory does not exist.
*/
force?: boolean;
};
/**
* Removes files and directories (synchronously). Supports globs.
*/
export declare const rm: (path: string, options?: RmOptions) => void;
export declare const chalk: _chalk.Chalk & _chalk.ChalkFunction & {
supportsColor: false | _chalk.ColorSupport;
Level: _chalk.Level;
Color: ("black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | "grey" | "blackBright" | "redBright" | "greenBright" | "yellowBright" | "blueBright" | "magentaBright" | "cyanBright" | "whiteBright") | ("bgBlack" | "bgRed" | "bgGreen" | "bgYellow" | "bgBlue" | "bgMagenta" | "bgCyan" | "bgWhite" | "bgGray" | "bgGrey" | "bgBlackBright" | "bgRedBright" | "bgGreenBright" | "bgYellowBright" | "bgBlueBright" | "bgMagentaBright" | "bgCyanBright" | "bgWhiteBright");
ForegroundColor: "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | "grey" | "blackBright" | "redBright" | "greenBright" | "yellowBright" | "blueBright" | "magentaBright" | "cyanBright" | "whiteBright";
BackgroundColor: "bgBlack" | "bgRed" | "bgGreen" | "bgYellow" | "bgBlue" | "bgMagenta" | "bgCyan" | "bgWhite" | "bgGray" | "bgGrey" | "bgBlackBright" | "bgRedBright" | "bgGreenBright" | "bgYellowBright" | "bgBlueBright" | "bgMagentaBright" | "bgCyanBright" | "bgWhiteBright";
Modifiers: "bold" | "reset" | "dim" | "italic" | "underline" | "inverse" | "hidden" | "strikethrough" | "visible";
stderr: _chalk.Chalk & {
supportsColor: false | _chalk.ColorSupport;
};
};
export declare type SetupConfig = {
file: string;
/**
* If true, CLI will only accept a single target to run and will receive all
* passed arguments as is (not only flags).
*/
singleTarget?: boolean;
};
/**
* 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: SetupConfig) => Promise<number>;
export declare const sleep: (time: number) => Promise<unknown>;
export {};

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
{
"private": true,
"type": "commonjs"
}

115
tools/build/lib/byond.js Normal file
View File

@@ -0,0 +1,115 @@
import fs from 'fs';
import path from 'path';
import Juke from '../juke/index.js';
import { regQuery } from './winreg.js';
/**
* Cached path to DM compiler
*/
let dmPath;
const getDmPath = async () => {
if (dmPath) {
return 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 {
return fs.statSync(path).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'
);
})();
return dmPath;
};
/**
* @param {string} dmeFile
* @param {{ defines?: string[] }} options
*/
export const DreamMaker = async (dmeFile, options = {}) => {
const dmPath = await getDmPath();
// Get project basename
const dmeBaseName = dmeFile.replace(/\.dme$/, '');
// Make sure output files are writable
const testOutputFile = (name) => {
try {
fs.closeSync(fs.openSync(name, 'r+'));
}
catch (err) {
if (err && err.code === 'ENOENT') {
return;
}
if (err && err.code === 'EBUSY') {
Juke.logger.error(`File '${name}' is locked by the DreamDaemon process.`);
Juke.logger.error(`Stop the currently running server and try again.`);
throw new Juke.ExitCode(1);
}
throw err;
}
};
testOutputFile(`${dmeBaseName}.dmb`);
testOutputFile(`${dmeBaseName}.rsc`);
// Compile
const { defines } = options;
if (defines && defines.length > 0) {
const injectedContent = defines
.map(x => `#define ${x}\n`)
.join('');
fs.writeFileSync(`${dmeBaseName}.m.dme`, injectedContent);
const dmeContent = fs.readFileSync(`${dmeBaseName}.dme`);
fs.appendFileSync(`${dmeBaseName}.m.dme`, dmeContent);
await Juke.exec(dmPath, [`${dmeBaseName}.m.dme`]);
fs.writeFileSync(`${dmeBaseName}.dmb`, fs.readFileSync(`${dmeBaseName}.m.dmb`));
fs.writeFileSync(`${dmeBaseName}.rsc`, fs.readFileSync(`${dmeBaseName}.m.rsc`));
fs.unlinkSync(`${dmeBaseName}.m.dmb`);
fs.unlinkSync(`${dmeBaseName}.m.rsc`);
fs.unlinkSync(`${dmeBaseName}.m.dme`);
}
else {
await Juke.exec(dmPath, [dmeFile]);
}
};
export const DreamDaemon = async (dmbFile, ...args) => {
const dmPath = await getDmPath();
const baseDir = path.dirname(dmPath);
const ddExeName = process.platform === 'win32' ? 'dd.exe' : 'DreamDaemon';
const ddExePath = baseDir === '.' ? ddExeName : path.join(baseDir, ddExeName);
return Juke.exec(ddExePath, [dmbFile, ...args]);
};

View File

@@ -4,14 +4,14 @@
* Adapted from `tgui/packages/tgui-dev-server/winreg.js`.
*
* @file
* @copyright 2020 Aleksej Komarov
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
const { exec } = require('child_process');
const { promisify } = require('util');
import { exec } from 'child_process';
import { promisify } from 'util';
const regQuery = async (path, key) => {
export const regQuery = async (path, key) => {
if (process.platform !== 'win32') {
return null;
}
@@ -40,7 +40,3 @@ const regQuery = async (path, key) => {
return null;
}
};
module.exports = {
regQuery,
};

13
tools/build/lib/yarn.js Normal file
View File

@@ -0,0 +1,13 @@
import Juke from '../juke/index.js';
let yarnPath;
export const yarn = (...args) => {
if (!yarnPath) {
yarnPath = Juke.glob('./tgui/.yarn/releases/*.cjs')[0]
.replace('/tgui/', '/');
}
return Juke.exec('node', [yarnPath, ...args], {
cwd: './tgui',
});
};

4
tools/build/package.json Normal file
View File

@@ -0,0 +1,4 @@
{
"private": true,
"type": "module"
}