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

View File

@@ -1,2 +1,3 @@
@call tools\build\build @echo off
@pause 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. 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 ## Dependencies
- On Windows, `BUILD.bat` will automatically install a private (vendored) copy of Node. - 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? ## 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. 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 #!/bin/sh
#Build TGUI
set -e set -e
cd "$(dirname "$0")" cd "$(dirname "$0")"
exec ../bootstrap/node build.js "$@" 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 #!/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 * @file
* @copyright 2020 Aleksej Komarov * @copyright 2021 Aleksej Komarov
* @license MIT * @license MIT
*/ */
// Change working directory to project root import fs from 'fs';
process.chdir(require('path').resolve(__dirname, '../../')); import { DreamDaemon, DreamMaker } from './lib/byond.js';
import { yarn } from './lib/yarn.js';
import Juke from './juke/index.js';
// Validate NodeJS version Juke.chdir('../..', import.meta.url);
const NODE_VERSION = parseInt(process.versions.node.match(/(\d+)/)[1]); Juke.setup({ file: import.meta.url }).then((code) => process.exit(code));
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);
}
const STANDARD_BUILD = "Standard Build" const DME_NAME = 'tgstation';
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; export const DefineParameter = new Juke.Parameter({
if (process.env.CBT_BUILD_MODE) { type: 'string[]',
switch (process.env.CBT_BUILD_MODE) { alias: 'D',
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' export const PortParameter = new Juke.Parameter({
type: 'string',
alias: 'p',
});
// Main export const CiParameter = new Juke.Parameter({
// -------------------------------------------------------- type: 'boolean',
});
const { resolveGlob, stat } = require('./cbt/fs'); export const DmMapsIncludeTarget = new Juke.Target({
const { exec } = require('./cbt/process'); executes: async () => {
const { Task, runTasks } = require('./cbt/task'); const folders = [
const { regQuery } = require('./cbt/winreg'); ...Juke.glob('_maps/RandomRuins/**/*.dmm'),
const fs = require('fs'); ...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);
},
});
const yarn = args => { export const DmTarget = new Juke.Target({
const yarnPath = resolveGlob('./tgui/.yarn/releases/yarn-*.cjs')[0] dependsOn: ({ get }) => [
.replace('/tgui/', '/'); get(DefineParameter).includes('ALL_MAPS') && DmMapsIncludeTarget,
return exec('node', [yarnPath, ...args], { ],
cwd: './tgui', 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(', '));
}
await DreamMaker(`${DME_NAME}.dme`, {
defines: ['CBT', ...defines],
});
},
});
/** Installs all tgui dependencies */ export const DmTestTarget = new Juke.Target({
const taskYarn = new Task('yarn') dependsOn: ({ get }) => [
// The following dependencies skip what could be considered an important get(DefineParameter).includes('ALL_MAPS') && DmMapsIncludeTarget,
// step in Yarn: it verifies the integrity of cache. With this setup, if ],
// cache ever becomes corrupted, your only option is to clean build. executes: async ({ get }) => {
.depends('tgui/.yarn/+(cache|releases|plugins|sdks)/**/*') const defines = get(DefineParameter);
.depends('tgui/**/package.json') if (defines.length > 0) {
.depends('tgui/yarn.lock') Juke.logger.info('Using defines:', defines.join(', '));
// Phony target (automatically created at the end of the task) }
.provides('tgui/.yarn/install-target') fs.copyFileSync(`${DME_NAME}.dme`, `${DME_NAME}.test.dme`);
.build(() => yarn(['install'])); 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);
}
},
});
/** Builds svg fonts */ export const YarnTarget = new Juke.Target({
const taskTgfont = new Task('tgfont') inputs: [
.depends('tgui/.yarn/install-target') 'tgui/.yarn/+(cache|releases|plugins|sdks)/**/*',
.depends('tgui/packages/tgfont/**/*.+(js|cjs|svg)') 'tgui/**/package.json',
.depends('tgui/packages/tgfont/package.json') 'tgui/yarn.lock',
.provides('tgui/packages/tgfont/dist/tgfont.css') ],
.provides('tgui/packages/tgfont/dist/tgfont.eot') outputs: [
.provides('tgui/packages/tgfont/dist/tgfont.woff2') 'tgui/.yarn/install-target',
.build(() => yarn(['workspace', 'tgfont', 'build'])); ],
executes: async () => {
await yarn('install');
},
});
/** Builds tgui */ export const TgFontTarget = new Juke.Target({
const taskTgui = new Task('tgui') dependsOn: [YarnTarget],
.depends('tgui/.yarn/install-target') inputs: [
.depends('tgui/webpack.config.js') 'tgui/.yarn/install-target',
.depends('tgui/**/package.json') 'tgui/packages/tgfont/**/*.+(js|cjs|svg)',
.depends('tgui/packages/**/*.+(js|cjs|ts|tsx|scss)') 'tgui/packages/tgfont/package.json',
.provides('tgui/public/tgui.bundle.css') ],
.provides('tgui/public/tgui.bundle.js') outputs: [
.provides('tgui/public/tgui-common.bundle.js') 'tgui/packages/tgfont/dist/tgfont.css',
.provides('tgui/public/tgui-panel.bundle.css') 'tgui/packages/tgfont/dist/tgfont.eot',
.provides('tgui/public/tgui-panel.bundle.js') 'tgui/packages/tgfont/dist/tgfont.woff2',
.build(async () => { ],
await yarn(['run', 'webpack-cli', '--mode=production']); 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. * Prepends the defines to the .dme.
* Does not clean them up, as this is intended for TGS which * Does not clean them up, as this is intended for TGS which
* clones new copies anyway. * clones new copies anyway.
*/ */
const taskPrependDefines = (...defines) => new Task('prepend-defines') const prependDefines = (...defines) => {
.build(async () => { const dmeContents = fs.readFileSync(`${DME_NAME}.dme`);
const dmeContents = fs.readFileSync(`${DME_NAME}.dme`); const textToWrite = defines.map(define => `#define ${define}\n`);
const textToWrite = defines.map(define => `#define ${define}\n`); fs.writeFileSync(`${DME_NAME}.dme`, `${textToWrite}\n${dmeContents}`);
fs.writeFileSync(`${DME_NAME}.dme`, `${textToWrite}\n${dmeContents}`); };
});
const taskDm = (...injectedDefines) => new Task('dm') export const TgsTarget = new Juke.Target({
.depends('_maps/map_files/generic/**') dependsOn: [TguiTarget, TgFontTarget],
.depends('code/**') executes: async () => {
.depends('goon/**') Juke.logger.info('Prepending TGS define');
.depends('html/**') prependDefines('TGS');
.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`]);
}
});
// Frontend const TGS_MODE = process.env.CBT_BUILD_MODE === 'TGS';
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); 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`. * Adapted from `tgui/packages/tgui-dev-server/winreg.js`.
* *
* @file * @file
* @copyright 2020 Aleksej Komarov * @copyright 2021 Aleksej Komarov
* @license MIT * @license MIT
*/ */
const { exec } = require('child_process'); import { exec } from 'child_process';
const { promisify } = require('util'); import { promisify } from 'util';
const regQuery = async (path, key) => { export const regQuery = async (path, key) => {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
return null; return null;
} }
@@ -40,7 +40,3 @@ const regQuery = async (path, key) => {
return null; 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"
}