Merge pull request #25 from SpraxDev/prepare-v1

Prepare for v1

* Use SpraxDev/Spgito-BuildTools by default
* Allow choosing between SpraxDev and SpigotMC BuildTools
* Improve logging and error handling
* Fix build being skipped when only one version should be build
* README.md: Fixed typos and updated description
* Improve GitHub-Actions
This commit is contained in:
Christian Koop 2020-11-24 19:29:36 +01:00 committed by GitHub
commit fc9adaeb2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 349 additions and 188 deletions

View File

@ -10,24 +10,43 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- run: | - name: Install dependencies and build
run: |
npm i npm i
npm run build npm run build
# Make sure the action works on a clean machine without building # Make sure the action works on a clean machine without building it
run: run:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: ./
- name: Compile 2 spigot version
uses: ./
with: with:
versions: latest, 1.8 versions: latest, 1.8
# Run again. The Action should detect that the requested versions are already inside the local maven repo
- name: Compile the same versions agains
uses: ./
with:
# These versions should match the ones above
versions: latest, 1.8
- name: Upload logs
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: logs
path: /tmp/SpraxDev-Action-SpigotMC/logs/
# Run the original BuildTools in GitHub Actions to easily compare the build times etc. # Run the original BuildTools in GitHub Actions to easily compare the build times etc.
original-run: original-run:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Run original Spigot-BuildTools - name: Run original Spigot-BuildTools
# These versions should match the ones from the 'run'-job
# Using '--compile Spigot' as this action does the same by default
run: | run: |
wget https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar -O BuildTools.jar wget https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar -O BuildTools.jar
java -jar BuildTools.jar --rev latest --compile Spigot java -jar BuildTools.jar --rev latest --compile Spigot

View File

@ -18,10 +18,14 @@
</a> </a>
</p> </p>
# Action-Spigot # Action-SpigotMC
This Action allows you to easily compile Minecraft Spigot This Action allows you to easily compile Minecraft Spigot or Paper
and install it in your runners local maven repository. and install it in your runners local maven repository.
Supported:
* SpigotMC (using official BuildTools or my modified one)
* ~~PaperMC~~ (coming soon #26)
You configure all the versions you want, and it'll compile all the missing versions automatically. You configure all the versions you want, and it'll compile all the missing versions automatically.
By checking for a file in the local maven repository beforehand, build times can be reduces drastically. By checking for a file in the local maven repository beforehand, build times can be reduces drastically.
@ -37,7 +41,7 @@ If you don't change them, you can remove them from your workflow,
as they are set automatically. as they are set automatically.
```YAML ```YAML
- uses: SpraxDev/Action-Spigot@v1 - uses: SpraxDev/Action-SpigotMC@v1
with: with:
# A comma-separated list of Spigot version that should be compiled # A comma-separated list of Spigot version that should be compiled
# These values are later given to the BuildTools.jar as '--rev' argument # These values are later given to the BuildTools.jar as '--rev' argument
@ -45,13 +49,6 @@ as they are set automatically.
# Example: latest, 1.14.4, 1.8.8 # Example: latest, 1.14.4, 1.8.8
versions: latest versions: latest
# A comma-separated list of build targets
# This value is later given to the BuildTools.jar as '--compile' argument
#
# Available: None, Spigot, CraftBukkit
# Example: Spigot, CraftBukkit
target: Spigot
# Should sources be generated? # Should sources be generated?
# If enabled, BuildTools is provided the '--generate-source' argument # If enabled, BuildTools is provided the '--generate-source' argument
generateSrc: false generateSrc: false
@ -72,4 +69,9 @@ as they are set automatically.
# The amount of builds allowed to run at the same time # The amount of builds allowed to run at the same time
# Set to '-1' to use system's cpu core count # Set to '-1' to use system's cpu core count
threads: -1 threads: -1
# You can choose between different BuildTools to be used by this action
# 'SpraxDev' is my fork of SpigotMC's that introduces some changes (https://github.com/SpraxDev/Spigot-BuildTools/#breaking-changes)
# Available: SpraxDev, SpigotMC
buildToolProvider: SpraxDev
``` ```

View File

@ -1,5 +1,5 @@
name: Compile Minecraft Spigot (BuildTools) name: Compile Minecraft Spigot or Paper (BuildTools)
description: Makes it easier to compile multiple Spigot versions at the same time and speed up clean builds description: Makes it easier to compile multiple Spigot/Paper versions at the same time and speed up clean builds
author: Christian Koop author: Christian Koop
branding: branding:
@ -11,10 +11,6 @@ inputs:
required: false required: false
default: latest default: latest
description: Versions to build (sperate multiple with ',') description: Versions to build (sperate multiple with ',')
target:
required: false
default: Spigot
description: Select what exactly you want to compile (none, Spigot, CraftBukkit) (sperate multiple with ',')
generateSrc: generateSrc:
required: false required: false
default: 'false' default: 'false'
@ -35,6 +31,10 @@ inputs:
required: false required: false
default: '-1' default: '-1'
description: The amount of builds allowed to run at a time, set to '-1' to use system's cpu count description: The amount of builds allowed to run at a time, set to '-1' to use system's cpu count
buildToolProvider:
required: false
default: SpraxDev
description: Whose BuildTool should be used? (SpraxDev [default], SpigotMC)
runs: runs:
using: node12 using: node12

2
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

4
dist/package.json generated vendored
View File

@ -33,12 +33,14 @@
"@actions/core": "^1.2.6", "@actions/core": "^1.2.6",
"async": "^3.2.0", "async": "^3.2.0",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"read-last-lines": "^1.7.2" "n-readlines": "^1.0.1",
"xml-js": "^1.6.11"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node12": "^1.0.7", "@tsconfig/node12": "^1.0.7",
"@types/async": "^3.2.4", "@types/async": "^3.2.4",
"@types/fs-extra": "^9.0.4", "@types/fs-extra": "^9.0.4",
"@types/n-readlines": "^1.0.1",
"@types/node": "~12.19.6", "@types/node": "~12.19.6",
"@vercel/ncc": "^0.25.1", "@vercel/ncc": "^0.25.1",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",

67
package-lock.json generated
View File

@ -30,6 +30,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/n-readlines": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/n-readlines/-/n-readlines-1.0.1.tgz",
"integrity": "sha512-n1ND4k+9hgtil2HnHK+1tIj1UWchsD+RwoH7eVQqLbUe8AxwLuqDEnx3Riq+twHoMTjP80Q/ilB3LGdyXKfTEg==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": { "@types/node": {
"version": "12.19.6", "version": "12.19.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.6.tgz",
@ -42,11 +51,6 @@
"integrity": "sha512-dGecC5+1wLof1MQpey4+6i2KZv4Sfs6WfXkl9KfO32GED4ZPiKxRfvtGPjbjZv0IbqMl6CxtcV1RotXYfd5SSA==", "integrity": "sha512-dGecC5+1wLof1MQpey4+6i2KZv4Sfs6WfXkl9KfO32GED4ZPiKxRfvtGPjbjZv0IbqMl6CxtcV1RotXYfd5SSA==",
"dev": true "dev": true
}, },
"any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
},
"arg": { "arg": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@ -113,28 +117,15 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true "dev": true
}, },
"mz": { "n-readlines": {
"version": "2.7.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "resolved": "https://registry.npmjs.org/n-readlines/-/n-readlines-1.0.1.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "integrity": "sha512-z4SyAIVgMy7CkgsoNw7YVz40v0g4+WWvvqy8+ZdHrCtgevcEO758WQyrYcw3XPxcLxF+//RszTz/rO48nzD0wQ=="
"requires": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
"thenify-all": "^1.0.0"
}
}, },
"object-assign": { "sax": {
"version": "4.1.1", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"read-last-lines": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/read-last-lines/-/read-last-lines-1.7.2.tgz",
"integrity": "sha512-K0yUvTYAYn6qpyLJufaJ7yC6BeL23qpgZ8SKM7/fA1R1rHotCDxB/zDp9i1I2JHvexWBW6/35jkt07iiIKKp4g==",
"requires": {
"mz": "^2.7.0"
}
}, },
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
@ -152,22 +143,6 @@
"source-map": "^0.6.0" "source-map": "^0.6.0"
} }
}, },
"thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"requires": {
"any-promise": "^1.0.0"
}
},
"thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=",
"requires": {
"thenify": ">= 3.1.0 < 4"
}
},
"ts-node": { "ts-node": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz",
@ -192,6 +167,14 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug=="
}, },
"xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"requires": {
"sax": "^1.2.4"
}
},
"yn": { "yn": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@ -33,12 +33,14 @@
"@actions/core": "^1.2.6", "@actions/core": "^1.2.6",
"async": "^3.2.0", "async": "^3.2.0",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"read-last-lines": "^1.7.2" "n-readlines": "^1.0.1",
"xml-js": "^1.6.11"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node12": "^1.0.7", "@tsconfig/node12": "^1.0.7",
"@types/async": "^3.2.4", "@types/async": "^3.2.4",
"@types/fs-extra": "^9.0.4", "@types/fs-extra": "^9.0.4",
"@types/n-readlines": "^1.0.1",
"@types/node": "~12.19.6", "@types/node": "~12.19.6",
"@vercel/ncc": "^0.25.1", "@vercel/ncc": "^0.25.1",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",

View File

@ -1,130 +1,225 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import { join as joinPath, resolve as resolvePath } from 'path';
import { copy } from 'fs-extra';
import { rmdirSync } from 'fs';
import { cpuCount, downloadFile, exit, fixArgArr, isNumeric, resetWorkingDir, runCmd } from './utils';
import { parallelLimit } from 'async'; import { parallelLimit } from 'async';
import { createWriteStream, existsSync, rmdirSync } from 'fs';
import { copy } from 'fs-extra';
import { join as joinPath, resolve as resolvePath } from 'path';
import { xml2js } from 'xml-js';
import {
cpuCount,
downloadFile,
exit,
fixArgArr,
isNumeric,
readLastLines,
resetWorkingDir,
runCmd,
userHomeDir
} from './utils';
const rll = require('read-last-lines'); const supportedBuildTools: { [key: string]: { url: string, prepareArgs: string[] } } = {
spraxdev: {
url: 'https://github.com/SpraxDev/Spigot-BuildTools/releases/latest/download/BuildTools.jar',
prepareArgs: ['--exit-after-fetch']
},
spigotmc: {
url: 'https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar',
prepareArgs: ['--compile', 'None']
}
};
/* GitHub Actions inputs */ /* GitHub Actions inputs */
const versions: string[] = fixArgArr((core.getInput('versions') || 'latest').split(',')); const buildToolProvider: string = (core.getInput('buildToolProvider') || 'SpraxDev').toLowerCase();
const target: string[] = fixArgArr((core.getInput('target') || 'Spigot').toUpperCase().split(',')); let versions: string[] = fixArgArr((core.getInput('versions') || 'latest').toLowerCase().split(','));
const generateSrc: boolean = core.getInput('generateSrc') == 'true'; const generateSrc: boolean = core.getInput('generateSrc') == 'true';
const generateDoc: boolean = core.getInput('generateDoc') == 'true'; const generateDoc: boolean = core.getInput('generateDoc') == 'true';
const disableJavaCheck: boolean = core.getInput('disableJavaCheck') == 'true'; const disableJavaCheck: boolean = core.getInput('disableJavaCheck') == 'true';
const forceRun: boolean = core.getInput('forceRun') == 'true'; // TODO const forceRun: boolean = core.getInput('forceRun') == 'true';
const threadCount: number = isNumeric(core.getInput('threads')) ? parseInt(core.getInput('threads')) : cpuCount; const threadCount: number = isNumeric(core.getInput('threads')) ? parseInt(core.getInput('threads')) : cpuCount;
const workingDir = resetWorkingDir(); const workingDir = resetWorkingDir();
const appLogFile = joinPath(workingDir.logs, 'SpraxDev_Actions-SpigotMC.log');
const appLogStream = createWriteStream(appLogFile, {encoding: 'utf-8', flags: 'a' /* append */});
async function run(): Promise<{ code: number, msg?: string }> { async function run(): Promise<{ code: number, msg?: string }> {
return new Promise<{ code: number, msg?: string }>(async (resolve, reject): Promise<void> => { return new Promise(async (resolve, reject): Promise<void> => {
if (versions.length == 0) return resolve({code: 0, msg: 'No version(s) provided to build'}); try {
if (target.length == 0) return resolve({code: 0, msg: 'No target(s) provided to build'}); if (versions.length == 0) return resolve({code: 0, msg: 'No version(s) provided to build'});
const appLogFile = joinPath(workingDir.logs, 'SpraxDev_Actions-SpigotMC.log'); if (!Object.keys(supportedBuildTools).includes(buildToolProvider)) {
return reject(new Error(`'${buildToolProvider}' is not a valid BuildTool-Provider (${Object.keys(supportedBuildTools).join(', ')})`));
}
console.log('Installed Java-Version:'); if (!forceRun) {
await runCmd('java', ['-version'], workingDir.base, appLogFile); versions = await removeExistingVersions(versions, (ver, jarPath) => {
logInfo(`Skipping version '${ver}' because it has been found in the local maven repository: ${jarPath}`);
});
console.log(`Downloading BuildTools.jar from 'hub.spigotmc.org'...`); if (versions.length == 0) return resolve({code: 0, msg: 'No new versions to build'});
await downloadFile('https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar', joinPath(workingDir.cache, 'BuildTools.jar')); }
const gotTemplateDirectory = versions.length != 1; const buildTool = supportedBuildTools[buildToolProvider];
// Prepare template directory if more than one version is provided logInfo('Installed Java-Version:');
if (gotTemplateDirectory) { await runCmd('java', ['-version'], workingDir.base, appLogStream);
console.log('Prepare for future tasks by running BuildTools...');
logInfo(`\nDownloading '${buildTool.url}'...`);
await downloadFile(buildTool.url, joinPath(workingDir.cache, 'BuildTools.jar'));
const gotTemplateDirectory = versions.length != 1;
// Prepare template directory if more than one version is provided
if (gotTemplateDirectory) {
logInfo('Prepare for future tasks by running BuildTools...');
await core.group('Prepare BuildTools', async (): Promise<void> => {
try {
return runCmd('java', ['-jar', 'BuildTools.jar', (disableJavaCheck ? '--disable-java-check' : ''), ...buildTool.prepareArgs],
workingDir.cache, appLogStream);
} catch (err) {
logError(err);
logError(`\nPrinting last 30 lines from '${resolvePath(appLogFile)}':`);
for (const line of readLastLines(appLogFile, 30)) {
logError(line);
}
return exit(1);
}
});
}
const buildToolsArgs = ['-jar', 'BuildTools.jar', '--compile', 'Spigot'];
if (generateSrc) {
buildToolsArgs.push('--generate-source');
}
if (generateDoc) {
buildToolsArgs.push('--generate-docs');
}
if (disableJavaCheck) {
buildToolsArgs.push('--disable-java-check');
}
const tasks = [];
for (const ver of versions) {
tasks.push(async (): Promise<void> => {
return new Promise(async (resolveTask, rejectTask): Promise<void> => {
const start = Date.now();
const logFile = joinPath(workingDir.logs, `${ver}.log`);
logInfo(`Building version '${ver}'...`);
// If there is only one version to build, the cache directory is used instead of copying it first
const versionDir = gotTemplateDirectory ? joinPath(workingDir.base, `${ver}`) : workingDir.cache;
if (gotTemplateDirectory) {
await copy(workingDir.cache, versionDir);
}
try {
// set to silent because multiple builds can run at once
await runCmd('java', [...buildToolsArgs, '--rev', ver], versionDir, logFile, true);
if (gotTemplateDirectory) {
rmdirSync(versionDir, {recursive: true}); // delete our task dir
}
const end = Date.now();
logInfo(`Finished '${ver}' in ${((end - start) / 60_000).toFixed(2)} minutes`);
resolveTask();
} catch (err) {
logInfo(`An error occurred while building '${ver}'`);
logError(err);
logError(`\nPrinting last 30 lines from '${resolvePath(logFile)}':`);
for (const line of readLastLines(logFile, 30)) {
logError(line);
}
rejectTask(err);
}
});
});
}
parallelLimit(tasks, threadCount, (err) => {
if (err) return reject(err);
resolve({code: 0});
});
} catch (err) {
reject(err);
}
});
}
async function removeExistingVersions(versionArr: string[], onExist: (ver: string, jarPath: string) => void): Promise<string[]> {
return new Promise(async (resolve, _reject): Promise<void> => {
const result = [];
for (const ver of versionArr) {
let skipVersion = false;
let versionToCheck: string | null = ver != 'latest' ? ver : null;
try { try {
await core.group('Prepare BuildTools', async (): Promise<void> => { const verJsonBuff = await downloadFile(`https://hub.spigotmc.org/versions/${ver}.json`, null);
return runCmd('java', ['-jar', 'BuildTools.jar', '--compile', 'NONE', (disableJavaCheck ? '--disable-java-check' : '')], const verJson = verJsonBuff instanceof Buffer ? JSON.parse(verJsonBuff.toString('utf-8')) : null;
workingDir.cache, appLogFile); const bukkitRef: undefined | string = verJson?.refs?.Bukkit;
});
} catch (err) {
console.error(err);
console.error(`\nPrinting last 25 lines from '${resolvePath(appLogFile)}':`); if (bukkitRef) {
for (const line of (await rll.read(appLogFile, 25))) { const verPomBuff = await downloadFile(`https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/raw/pom.xml?at=${bukkitRef}`, null);
console.error(line);
if (verPomBuff instanceof Buffer) {
const result = xml2js(verPomBuff.toString('utf-8'), {
compact: true,
ignoreComment: true,
ignoreAttributes: true
}) as any;
versionToCheck = result.project?.version?._text;
}
} }
} catch (err) {
logError(err);
}
return exit(1); const jarPath = resolvePath(joinPath(userHomeDir, `/.m2/repository/org/spigotmc/spigot/${versionToCheck}/spigot-${versionToCheck}.jar`));
if (versionToCheck) {
skipVersion = existsSync(jarPath);
}
if (skipVersion) {
onExist(ver, jarPath);
} else {
result.push(ver);
} }
} }
const buildToolsArgs = ['-jar', 'BuildTools.jar', '--compile', target.join(',')]; resolve(result);
if (generateSrc) {
buildToolsArgs.push('--generate-source');
}
if (generateDoc) {
buildToolsArgs.push('--generate-docs');
}
if (disableJavaCheck) {
buildToolsArgs.push('--disable-java-check');
}
const tasks = [];
for (const ver of versions) {
tasks.push((callback: (err?: Error, result?: unknown) => void) => {
try {
const start = Date.now();
const logFile = joinPath(workingDir.logs, `${ver}.log`);
console.log(`Building version '${ver}'...`);
// If there is only one version to build, the cache directory is used instead of copying it first
const versionDir = gotTemplateDirectory ? joinPath(workingDir.base, `${ver}`) : workingDir.cache;
if (gotTemplateDirectory) {
copy(workingDir.cache, versionDir)
.then(() => {
runCmd('java', [...buildToolsArgs, '--rev', ver],
versionDir, logFile, true) // set to silent because multiple builds can run at once
.then(() => {
rmdirSync(versionDir, {recursive: true}); // delete our task dir
const end = Date.now();
console.log(`Finished building '${ver}' in ${((end - start) / 60_000)} minutes`);
callback();
});
})
.catch((err) => {
console.log(`An error occurred while building '${ver}'`);
console.error(err);
console.error(`\nPrinting last 25 lines from '${resolvePath(logFile)}':`);
rll.read(logFile, 25)
.then((lines: string[]) => {
for (const line of lines) {
console.error(line);
}
})
.catch(console.error)
.finally(() => callback(err));
});
}
} catch (err) {
callback(err);
}
});
}
(parallelLimit(tasks, threadCount) as unknown as Promise<unknown[]>) // Valid according to docs - types outdated?
.then(() => resolve({code: 0}))
.catch(reject);
}); });
} }
export function logInfo(msg?: string): void {
console.log(msg);
appLogStream.write(msg + '\n');
}
export function logError(msg?: string | object): void {
if (typeof msg != 'string') {
msg = JSON.stringify(msg, null, 2);
}
console.error(msg);
appLogStream.write(msg + '\n');
}
run() run()
.then((result) => exit(result.code, result.msg)) .then((result) => exit(result.code, result.msg))
.catch((err) => exit(1, err)); .catch((err) => exit(1, err));

View File

@ -1,14 +1,17 @@
import { spawn as spawnProcess } from 'child_process'; import { spawn as spawnProcess } from 'child_process';
import { join as joinPath } from 'path'; import { createWriteStream, mkdirSync, readFileSync, rmdirSync, WriteStream } from 'fs';
import { get as httpGet } from 'http'; import { get as httpGet } from 'http';
import { get as httpsGet } from 'https'; import { get as httpsGet } from 'https';
import { cpus, tmpdir } from 'os'; import readLines from 'n-readlines';
import { createWriteStream, mkdirSync, readFileSync, rmdirSync, WriteStream } from 'fs'; import { cpus, homedir, tmpdir } from 'os';
import { join as joinPath } from 'path';
import { logError, logInfo } from './index';
const packageJson = JSON.parse(readFileSync(joinPath(__dirname, '..', 'package.json'), 'utf-8')); const packageJson = JSON.parse(readFileSync(joinPath(__dirname, '..', 'package.json'), 'utf-8'));
const userAgent = `${packageJson.name || 'Action-SpigotMC'}/${packageJson.version || 'UNKNOWN_VERSION'} (+${packageJson.homepage || 'https://github.com/SpraxDev/'})`; const userAgent = `${packageJson.name || 'Action-SpigotMC'}/${packageJson.version || 'UNKNOWN_VERSION'} (+${packageJson.homepage || 'https://github.com/SpraxDev/Action-SpigotMC'})`;
export const cpuCount = cpus().length; export const cpuCount = cpus().length;
export const userHomeDir = homedir();
export function fixArgArr(arr: string[]): string[] { export function fixArgArr(arr: string[]): string[] {
const result: string[] = []; const result: string[] = [];
@ -28,9 +31,12 @@ export function isNumeric(str: string): boolean {
return /^[0-9]+$/.test(str); return /^[0-9]+$/.test(str);
} }
export async function runCmd(cmd: string, args: string[], workingDir: string, logFile: string, silent: boolean = false): Promise<void> { export async function runCmd(cmd: string, args: string[], workingDir: string, logStreamOrFile: string | WriteStream, silent: boolean = false): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const logStream = createWriteStream(logFile, {encoding: 'utf-8', flags: 'a'}); // Use UTF-8 and append when file exists const closeLogStream = typeof logStreamOrFile == 'string';
const logStream = typeof logStreamOrFile != 'string' ? logStreamOrFile :
createWriteStream(logStreamOrFile, {encoding: 'utf-8', flags: 'a' /* append */});
const runningProcess = spawnProcess(cmd, args, {shell: true, cwd: workingDir, env: process.env}); const runningProcess = spawnProcess(cmd, args, {shell: true, cwd: workingDir, env: process.env});
runningProcess.stdout.on('data', (data) => { runningProcess.stdout.on('data', (data) => {
@ -49,7 +55,9 @@ export async function runCmd(cmd: string, args: string[], workingDir: string, lo
}); });
runningProcess.on('close', (code) => { runningProcess.on('close', (code) => {
logStream.close(); if (closeLogStream) {
logStream.close();
}
if (code != 0) { if (code != 0) {
return reject({err: new Error(`process exited with code ${code}`), cmd, workingDir}); return reject({err: new Error(`process exited with code ${code}`), cmd, workingDir});
@ -60,48 +68,81 @@ export async function runCmd(cmd: string, args: string[], workingDir: string, lo
}); });
} }
export async function downloadFile(url: string, dest: string): Promise<void> { /**
const getURL = url.toLowerCase().startsWith('http://') ? httpGet : httpsGet; * @param url The URL to fetch the data from
* @param dest Set to `null` to get an Buffer instead of writing it to the file system
* @param currRedirectDepth Internally used to track how often the function has been redirected
*/
export async function downloadFile(url: string, dest: string | null, currRedirectDepth: number = 0): Promise<Buffer | void> {
const doGetRequest = url.toLowerCase().startsWith('http://') ? httpGet : httpsGet;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let writeStream: WriteStream | null = null; let writeStream: WriteStream | null = null;
const done = function (err: boolean) { const done = function (errored: boolean) {
if (writeStream) { if (writeStream) {
writeStream.close(); writeStream.close();
writeStream = null; writeStream = null;
if (err) { if (errored && dest != null) {
rmdirSync(dest, {recursive: true}); rmdirSync(dest, {recursive: true});
} }
} }
}; };
// TODO doGetRequest(url, {
getURL(url, {
headers: { headers: {
'User-Agent': userAgent 'User-Agent': userAgent
} }
}, (httpRes) => { }, (httpRes) => {
if (httpRes.statusCode != 200) { if (httpRes.statusCode != 200) {
done(true); const locHeader = httpRes.headers.location;
return reject(new Error(`Server responded with ${httpRes.statusCode}`)); // Follow redirect
if (currRedirectDepth < 12 && locHeader &&
(httpRes.statusCode == 301 || httpRes.statusCode == 302 || httpRes.statusCode == 303 ||
httpRes.statusCode == 307 || httpRes.statusCode == 308)) {
done(false);
if (!/https?:\/\//g.test(locHeader)) {
return reject(new Error(`Server responded with ${httpRes.statusCode} and a relative Location-Header value (${locHeader})`));
}
return downloadFile(locHeader, dest, ++currRedirectDepth)
.then(resolve)
.catch(reject);
} else {
done(true);
return reject(new Error(`Server responded with ${httpRes.statusCode}`));
}
} }
writeStream = createWriteStream(dest, {encoding: 'binary'}) if (dest != null) {
.on('finish', () => { writeStream = createWriteStream(dest, {encoding: 'binary'})
done(false); .on('finish', () => {
done(false);
return resolve(); return resolve();
}) })
.on('error', (err) => { .on('error', (err) => {
done(true); done(true);
return reject(err); return reject(err);
}); });
httpRes.pipe(writeStream); httpRes.pipe(writeStream);
} else {
const chunks: Buffer[] = [];
httpRes.on('data', (chunk) => {
chunks.push(Buffer.from(chunk, 'binary'));
});
httpRes.on('end', () => {
resolve(Buffer.concat(chunks));
});
}
}) })
.on('error', (err) => { .on('error', (err) => {
done(true); done(true);
@ -111,6 +152,23 @@ export async function downloadFile(url: string, dest: string): Promise<void> {
}); });
} }
export function readLastLines(file: string, lineCount: number, encoding: string = 'utf-8'): string[] {
const result = [];
const reader = new readLines(file);
let line;
while (line = reader.next()) {
result.push(line.toString(encoding));
if (result.length > lineCount) {
result.shift();
}
}
return result;
}
export function resetWorkingDir(): { base: string, cache: string, logs: string } { export function resetWorkingDir(): { base: string, cache: string, logs: string } {
const baseDir = joinPath(tmpdir(), 'SpraxDev-Action-SpigotMC'); const baseDir = joinPath(tmpdir(), 'SpraxDev-Action-SpigotMC');
const cacheDir = joinPath(baseDir, 'cache'); const cacheDir = joinPath(baseDir, 'cache');
@ -128,9 +186,9 @@ export function resetWorkingDir(): { base: string, cache: string, logs: string }
export function exit(code: number, msg?: string | Error): never { export function exit(code: number, msg?: string | Error): never {
if (msg) { if (msg) {
if (typeof msg == 'string') { if (typeof msg == 'string') {
console.log(msg); logInfo(msg);
} else { } else {
console.error(msg); logError(msg);
} }
} }