oclif
Version:
oclif: create your own CLI
289 lines (263 loc) • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@oclif/core");
const path = require("path");
const fs = require("fs-extra");
const Tarballs = require("../../tarballs");
const upload_util_1 = require("../../upload-util");
const child_process_1 = require("child_process");
const node_util_1 = require("node:util");
const exec = (0, node_util_1.promisify)(child_process_1.exec);
const scripts = {
/* eslint-disable no-useless-escape */
// eslint-disable-next-line unicorn/no-useless-undefined
cmd: (config, additionalCLI = undefined) => ` off
setlocal enableextensions
set ${additionalCLI ? `${additionalCLI.toUpperCase()}_BINPATH` : config.scopedEnvVarKey('BINPATH')}=%~dp0\\${additionalCLI !== null && additionalCLI !== void 0 ? additionalCLI : config.bin}.cmd
if exist "%LOCALAPPDATA%\\${config.dirname}\\client\\bin\\${additionalCLI !== null && additionalCLI !== void 0 ? additionalCLI : config.bin}.cmd" (
"%LOCALAPPDATA%\\${config.dirname}\\client\\bin\\${additionalCLI !== null && additionalCLI !== void 0 ? additionalCLI : config.bin}.cmd" %*
) else (
"%~dp0\\..\\client\\bin\\node.exe" "%~dp0\\..\\client\\${additionalCLI ? `${additionalCLI}\\bin\\run` : 'bin\\run'}" %*
)
`,
sh: (config) => `#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')")
"$basedir/../client/bin/${config.bin}.cmd" "$@"
ret=$?
exit $ret
`,
nsis: (config, arch) => `!include MUI2.nsh
!define Version '${config.version.split('-')[0]}'
Name "${config.name}"
CRCCheck On
InstallDirRegKey HKCU "Software\\${config.name}" ""
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
OutFile "installer.exe"
VIProductVersion "\${VERSION}.0"
VIAddVersionKey /LANG=\${LANG_ENGLISH} "ProductName" "${config.name}"
VIAddVersionKey /LANG=\${LANG_ENGLISH} "Comments" "${config.pjson.homepage}"
VIAddVersionKey /LANG=\${LANG_ENGLISH} "CompanyName" "${config.scopedEnvVar('AUTHOR') || config.pjson.author}"
VIAddVersionKey /LANG=\${LANG_ENGLISH} "LegalCopyright" "${new Date().getFullYear()}"
VIAddVersionKey /LANG=\${LANG_ENGLISH} "FileDescription" "${config.pjson.description}"
VIAddVersionKey /LANG=\${LANG_ENGLISH} "FileVersion" "\${VERSION}.0"
VIAddVersionKey /LANG=\${LANG_ENGLISH} "ProductVersion" "\${VERSION}.0"
InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
Section "${config.name} CLI \${VERSION}"
SetOutPath $INSTDIR
File /r bin
File /r client
ExpandEnvStrings $0 "%COMSPEC%"
WriteRegStr HKCU "Software\\${config.dirname}" "" $INSTDIR
WriteUninstaller "$INSTDIR\\Uninstall.exe"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
"DisplayName" "${config.name}"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
"DisplayVersion" "\${VERSION}"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
"UninstallString" "$\\"$INSTDIR\\uninstall.exe$\\""
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
"Publisher" "${config.scopedEnvVar('AUTHOR') || config.pjson.author}"
SectionEnd
Section "Set PATH to ${config.name}"
Push "$INSTDIR\\bin"
Call AddToPath
SectionEnd
Section "Add %LOCALAPPDATA%\\${config.dirname} to Windows Defender exclusions (highly recommended for performance!)"
ExecShell "" '"$0"' "/C powershell -ExecutionPolicy Bypass -Command $\\"& {Add-MpPreference -ExclusionPath $\\"$LOCALAPPDATA\\${config.dirname}$\\"}$\\" -FFFeatureOff" SW_HIDE
SectionEnd
Section "Uninstall"
Delete "$INSTDIR\\Uninstall.exe"
RMDir /r "$INSTDIR"
RMDir /r "$LOCALAPPDATA\\${config.dirname}"
DeleteRegKey /ifempty HKCU "Software\\${config.dirname}"
DeleteRegKey HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}"
SectionEnd
!define Environ 'HKCU "Environment"'
Function AddToPath
Exch $0
Push $1
Push $2
Push $3
Push $4
; NSIS ReadRegStr returns empty string on string overflow
; Native calls are used here to check actual length of PATH
; $4 = RegOpenKey(HKEY_CURRENT_USER, "Environment", &$3)
System::Call "advapi32::RegOpenKey(i 0x80000001, t'Environment', *i.r3) i.r4"
IntCmp $4 0 0 done done
; $4 = RegQueryValueEx($3, "PATH", (DWORD*)0, (DWORD*)0, &$1, ($2=NSIS_MAX_STRLEN, &$2))
; RegCloseKey($3)
System::Call "advapi32::RegQueryValueEx(i $3, t'PATH', i 0, i 0, t.r1, *i \${NSIS_MAX_STRLEN} r2) i.r4"
System::Call "advapi32::RegCloseKey(i $3)"
IntCmp $4 234 0 +4 +4 ; $4 == ERROR_MORE_DATA
DetailPrint "AddToPath: original length $2 > \${NSIS_MAX_STRLEN}"
MessageBox MB_OK "PATH not updated, original length $2 > \${NSIS_MAX_STRLEN}"
Goto done
IntCmp $4 0 +5 ; $4 != NO_ERROR
IntCmp $4 2 +3 ; $4 != ERROR_FILE_NOT_FOUND
DetailPrint "AddToPath: unexpected error code $4"
Goto done
StrCpy $1 ""
; Check if already in PATH
Push "$1;"
Push "$0;"
Call StrStr
Pop $2
StrCmp $2 "" 0 done
Push "$1;"
Push "$0\\;"
Call StrStr
Pop $2
StrCmp $2 "" 0 done
; Prevent NSIS string overflow
StrLen $2 $0
StrLen $3 $1
IntOp $2 $2 + $3
IntOp $2 $2 + 2 ; $2 = strlen(dir) + strlen(PATH) + sizeof(";")
IntCmp $2 \${NSIS_MAX_STRLEN} +4 +4 0
DetailPrint "AddToPath: new length $2 > \${NSIS_MAX_STRLEN}"
MessageBox MB_OK "PATH not updated, new length $2 > \${NSIS_MAX_STRLEN}."
Goto done
; Append dir to PATH
DetailPrint "Add to PATH: $0"
StrCpy $2 $1 1 -1
StrCmp $2 ";" 0 +2
StrCpy $1 $1 -1 ; remove trailing ';'
StrCmp $1 "" +2 ; no leading ';'
StrCpy $0 "$1;$0"
WriteRegExpandStr \${Environ} "PATH" $0
SendMessage \${HWND_BROADCAST} \${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
done:
Pop $4
Pop $3
Pop $2
Pop $1
Pop $0
FunctionEnd
; StrStr - find substring in a string
;
; Usage:
; Push "this is some string"
; Push "some"
; Call StrStr
; Pop $0 ; "some string"
Function StrStr
Exch $R1 ; $R1=substring, stack=[old$R1,string,...]
Exch ; stack=[string,old$R1,...]
Exch $R2 ; $R2=string, stack=[old$R2,old$R1,...]
Push $R3
Push $R4
Push $R5
StrLen $R3 $R1
StrCpy $R4 0
; $R1=substring, $R2=string, $R3=strlen(substring)
; $R4=count, $R5=tmp
loop:
StrCpy $R5 $R2 $R3 $R4
StrCmp $R5 $R1 done
StrCmp $R5 "" done
IntOp $R4 $R4 + 1
Goto loop
done:
StrCpy $R1 $R2 "" $R4
Pop $R5
Pop $R4
Pop $R3
Pop $R2
Exch $R1 ; $R1=old$R1, stack=[result,...]
FunctionEnd
`,
/* eslint-enable no-useless-escape */
};
class PackWin extends core_1.Command {
async run() {
await this.checkForNSIS();
const { flags } = await this.parse(PackWin);
const buildConfig = await Tarballs.buildConfig(flags.root);
const { config } = buildConfig;
await Tarballs.build(buildConfig, { platform: 'win32', pack: false, tarball: flags.tarball, parallel: true });
const arches = buildConfig.targets.filter(t => t.platform === 'win32').map(t => t.arch);
await Promise.all(arches.map(async (arch) => {
const installerBase = path.join(buildConfig.tmp, `windows-${arch}-installer`);
await fs.promises.rm(installerBase, { recursive: true, force: true });
await fs.promises.mkdir(path.join(installerBase, 'bin'), { recursive: true });
await Promise.all([
fs.writeFile(path.join(installerBase, 'bin', `${config.bin}.cmd`), scripts.cmd(config)),
fs.writeFile(path.join(installerBase, 'bin', `${config.bin}`), scripts.sh(config)),
fs.writeFile(path.join(installerBase, `${config.bin}.nsi`), scripts.nsis(config, arch)),
].concat(flags['additional-cli'] ? [
fs.writeFile(path.join(installerBase, 'bin', `${flags['additional-cli']}.cmd`), scripts.cmd(config, flags['additional-cli'])),
fs.writeFile(path.join(installerBase, 'bin', `${flags['additional-cli']}`), scripts.sh({ bin: flags['additional-cli'] })),
] : []));
await fs.move(buildConfig.workspace({ platform: 'win32', arch }), path.join(installerBase, 'client'));
await exec(`makensis ${installerBase}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
const templateKey = (0, upload_util_1.templateShortKey)('win32', { bin: config.bin, version: config.version, sha: buildConfig.gitSha, arch });
const o = buildConfig.dist(`win32/${templateKey}`);
await fs.move(path.join(installerBase, 'installer.exe'), o);
const windows = config.pjson.oclif.windows;
if (windows && windows.name && windows.keypath) {
await signWindows(o, arch, config, windows);
}
else
this.debug('Skipping windows exe signing');
this.log(`built ${o}`);
}));
}
async checkForNSIS() {
try {
await exec('makensis');
}
catch (error) {
if (error.code === 1)
return;
if (error.code === 127)
this.error('install makensis');
else
throw error;
}
}
}
exports.default = PackWin;
PackWin.description = `create windows installer from oclif CLI
This command requires WINDOWS_SIGNING (prefixed with the name of your executable, e.g. OCLIF_WINDOWS_SIGNING_PASS) to be set in the environment`;
PackWin.flags = {
root: core_1.Flags.string({
char: 'r',
description: 'path to oclif CLI root',
default: '.',
required: true,
}),
'additional-cli': core_1.Flags.string({
description: `an Oclif CLI other than the one listed in config.bin that should be made available to the user
the CLI should already exist in a directory named after the CLI that is the root of the tarball produced by "oclif pack:tarballs"`,
hidden: true,
}),
tarball: core_1.Flags.string({
char: 't',
description: 'optionally specify a path to a tarball already generated by NPM',
required: false,
}),
};
async function signWindows(o, arch, config, windows) {
const buildLocationUnsigned = o.replace(`${arch}.exe`, `${arch}-unsigned.exe`);
await fs.move(o, buildLocationUnsigned);
const pass = config.scopedEnvVar('WINDOWS_SIGNING_PASS');
if (!pass) {
throw new Error(`${config.scopedEnvVarKey('WINDOWS_SIGNING_PASS')} not set in the environment`);
}
/* eslint-disable array-element-newline */
const args = [
'-pkcs12', windows.keypath,
'-pass', pass,
'-n', `"${windows.name}"`,
'-i', windows.homepage || config.pjson.homepage,
'-h', 'sha512',
'-in', buildLocationUnsigned,
'-out', o,
];
await exec(`osslsigncode sign ${args}.join(' ')`);
}