diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 979280f..f90a38f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -1,6 +1,3 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - name: Node.js CI on: @@ -12,16 +9,31 @@ on: jobs: build: - runs-on: windows-latest + runs-on: ${{ matrix.os }} strategy: matrix: + os: [ 'windows-latest', 'ubuntu-latest', 'macos-latest' ] node-version: [12.x] steps: - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + + - name: 'Install makensis (apt)' + run: sudo apt update && sudo apt install -y nsis nsis-pluginapi + if: ${{ matrix.os == 'ubuntu-latest' }} + + - name: 'Install makensis (homebrew)' + run: brew update && brew install makensis + if: ${{ matrix.os == 'macos-latest' }} + + - name: 'Clean up' + if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }} + run: rm -rf node_modules && npm install + - run: npm test diff --git a/README.md b/README.md index 38106f2..f984fa8 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,21 @@ # Nullsoft scriptable install system GitHub action -This action calls `C:\Program Files (x86)\NSIS\makensis.exe` to create an installer. This action is currently only compatible with Windows. +This action calls `makensis` to create a Windows installer. This codebase was ported from the Azure DevOps Extension [dev-maxima/nsis-extension][]. [dev-maxima/nsis-extension]: https://github.com/dev-maxima/nsis-extension +## Platforms + +This action looks for `makensis` or `makensis.exe` in the environment path, and if not found it will attempt to look in a couple of different places: + +* Windows - `C:\Program Files (x86)\NSIS\` +* Linux and macOS: + * `/usr/local/bin/` + * `/usr/bin/` + * `/opt/local/bin/` + ## Inputs ### `script-file` diff --git a/installer.js b/installer.js index 8275907..66eaf70 100644 --- a/installer.js +++ b/installer.js @@ -1,6 +1,6 @@ const fs = require('fs'); -const { execSync } = require('child_process'); const path = require('path'); +const makensis = require('./makensis'); const isDirectory = (item) => { const stats = fs.statSync(item); @@ -39,12 +39,6 @@ const copyDirectory = (src, dest) => { }); }; -const nsisInstallPath = path.join('C:', 'Program Files (x86)', 'NSIS'); - -const installerPathExists = () => { - return fs.existsSync(nsisInstallPath); -}; - class Installer { constructor(debugMode) { this.debugMode = debugMode; @@ -77,10 +71,10 @@ class Installer { const args = []; if (this.customArguments.indexOf('/V') === -1 && this.customArguments.indexOf('-V') === -1) { if (this.debugMode) { - args.push('/V4'); + args.push('-V4'); } else { - args.push('/V1'); + args.push('-V1'); } } @@ -91,27 +85,28 @@ class Installer { } createInstaller(scriptPath) { - if (!installerPathExists()) { - const installerPathMessage = `Installer path does not exist at ${nsisInstallPath}`; - this.debugLog(installerPathMessage); - throw new Error(installerPathMessage); - } - console.log(`Creating installer for: ${scriptPath}`); // Include any of the plugins that may have been requested. - const nsisPluginPath = path.join(nsisInstallPath, 'plugins'); - this.pluginPaths.forEach(pluginPath => { - console.log('Including plugin path', pluginPath); - copyDirectory(pluginPath, nsisPluginPath); - }); + if (this.pluginPaths.length) { + const nsisdir = makensis.getSymbols().NSISDIR; + if (!nsisdir) { + throw new Error('Unable to determine NSISDIR. Check makensis -HDRINFO output'); + } + const nsisPluginPath = path.join(nsisdir, 'Plugins'); + this.debugLog(`Using system Plugins path ${nsisPluginPath}`); + + this.pluginPaths.forEach(pluginPath => { + console.log('Including plugin path', pluginPath); + copyDirectory(pluginPath, nsisPluginPath); + }); + } - const args = this.getProcessArguments(scriptPath); - - const nsis3Exe = path.join(nsisInstallPath, 'makensis.exe'); - const makeCommand = `"${nsis3Exe}" ${args.join(' ')}`; - this.debugLog(`Running ${makeCommand}`); - const process = execSync(makeCommand); + const args = this.getProcessArguments(scriptPath) + .join(' '); + + this.debugLog(`Running ${args}`); + const _ = makensis.execSync(args); } }; diff --git a/makensis.js b/makensis.js new file mode 100644 index 0000000..57b1870 --- /dev/null +++ b/makensis.js @@ -0,0 +1,81 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); +const path = require('path'); +const { platform, env } = require('process'); + +const firstValidPath = (paths) => { + const possiblePaths = paths.filter(fs.existsSync); + + return possiblePaths.length ? possiblePaths[0] : ''; +}; + +const getWin32Path = () => { + const evaluationPaths = env.PATH.split(';').concat([ + path.join('C:', 'Program Files (x86)', 'NSIS') + ]).map(p => path.join(p, 'makensis.exe')); + + return firstValidPath( + evaluationPaths + ); +}; + +const getLinuxPath = () => { + const evaluationPaths = env.PATH.split(':').concat([ + '/usr/local/bin', + '/usr/bin', + '/opt/local/bin' + ]).map(p => path.join(p, 'makensis')); + + return firstValidPath(evaluationPaths); +}; + +class Makensis { + constructor(path) { + if (!fs.existsSync(path)) { + throw new Error('Unable to find makensis executable'); + } + this.path = path; + } + + execSync(args) { + const buffer = execSync(`"${this.path}" ${args}`); + + return buffer; + } + + getSymbols() { + const buffer = this.execSync('-HDRINFO'); + + // Look for the defined symbols in the output. + const exp = /Defined symbols: (.*)/g; + const matches = exp.exec(buffer.toString('utf-8')); + if (!matches || !matches.length || matches.length < 2) { + throw new Error('Unable to get symbols'); + } + + // Create a map of all of the symbols. + const symbols = { }; + + // Get all of the symbols, which are comma delimited + // keys or key value pairs separated by =. + matches[1].split(',').forEach(text => { + const index = text.indexOf('='); + if (index === -1) { + symbols[text] = ''; + } + else { + const name = text.substr(0, index); + const value = text.substr(index + 1); + symbols[name] = value; + } + }); + + return symbols; + } +} + +const makensisPath = platform === 'win32' + ? getWin32Path() + : getLinuxPath(); + +module.exports = new Makensis(makensisPath); diff --git a/node_modules/.bin/mocha b/node_modules/.bin/mocha old mode 100644 new mode 100755 diff --git a/node_modules/mocha/bin/_mocha b/node_modules/mocha/bin/_mocha old mode 100644 new mode 100755 diff --git a/node_modules/mocha/bin/mocha b/node_modules/mocha/bin/mocha old mode 100644 new mode 100755 diff --git a/package-lock.json b/package-lock.json index 3b5ed95..d88ae6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1335,6 +1335,12 @@ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "dev": true }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "dev": true + }, "yargs": { "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", diff --git a/package.json b/package.json index 3b51bd7..b87304a 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "chai": "^4.2.0", - "mocha": "^8.1.3" + "mocha": "^8.1.3", + "yaml": "^1.10.0" } } diff --git a/test/installer.spec.js b/test/installer.spec.js index 10817a5..ee2a640 100644 --- a/test/installer.spec.js +++ b/test/installer.spec.js @@ -36,7 +36,7 @@ describe('Installer', () => { const args = target.getProcessArguments(existingScriptPath); - expect(args).to.contain('/V1'); + expect(args).to.contain('-V1'); }); it('should default to all verbosity, given **debug** mode', () => { @@ -45,7 +45,7 @@ describe('Installer', () => { const args = target.getProcessArguments(existingScriptPath); - expect(args).to.contain('/V4'); + expect(args).to.contain('-V4'); }); it('should not add verbosity, given /V is in arguments', () => { @@ -56,7 +56,7 @@ describe('Installer', () => { const args = target.getProcessArguments(existingScriptPath); - expect(args).to.not.contain('/V1'); + expect(args).to.not.contain('-V1'); expect(args).to.contain('/V2'); }); @@ -68,7 +68,7 @@ describe('Installer', () => { const args = target.getProcessArguments(existingScriptPath); - expect(args).to.not.contain('/V1'); + expect(args).to.not.contain('-V1'); expect(args).to.contain('-V2'); });