UNPKG

oclif

Version:

oclif: create your own CLI

399 lines (369 loc) 16.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@oclif/core"); const fs_extra_1 = require("fs-extra"); const node_child_process_1 = require("node:child_process"); const node_fs_1 = require("node:fs"); const promises_1 = require("node:fs/promises"); const node_path_1 = __importDefault(require("node:path")); const node_util_1 = require("node:util"); const Tarballs = __importStar(require("../../tarballs")); const upload_util_1 = require("../../upload-util"); const exec = (0, node_util_1.promisify)(node_child_process_1.exec); const scripts = { /* eslint-disable no-useless-escape */ cmd: (config, additionalCLI, nodeOptions) => `@echo off setlocal enableextensions set ${additionalCLI ? `${additionalCLI.toUpperCase()}_BINPATH` : config.scopedEnvVarKey('BINPATH')}=%~dp0\\${additionalCLI ?? config.bin}.cmd if exist "%LOCALAPPDATA%\\${config.dirname}\\client\\bin\\${additionalCLI ?? config.bin}.cmd" ( "%LOCALAPPDATA%\\${config.dirname}\\client\\bin\\${additionalCLI ?? config.bin}.cmd" %* ) else ( "%~dp0\\..\\client\\bin\\node.exe" ${`${nodeOptions?.join(' ')} `}"%~dp0\\..\\client\\${additionalCLI ? `${additionalCLI}\\bin\\run` : String.raw `bin\run`}" %* ) `, nsis: ({ arch, config, customization, defenderOptional, hideDefenderOption, }) => `!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}" ${customization} Section "${config.name} CLI \${VERSION}" SetOutPath $INSTDIR File /r bin File /r client ; Use explicit System32/Sysnative path to cmd.exe for security StrCpy $0 "$WINDIR\\System32\\cmd.exe" ; Try System32 first IfFileExists "$0" path_is_safe StrCpy $0 "$WINDIR\\Sysnative\\cmd.exe" ; Try Sysnative for WOW64 IfFileExists "$0" path_is_safe MessageBox MB_OK|MB_ICONSTOP "Error: Could not find system cmd.exe. Installation cannot continue." Abort path_is_safe: 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 ${defenderOptional ? '/o ' : ''}"${hideDefenderOption ? '-' : ''}Add %LOCALAPPDATA%\\${config.dirname} to Windows Defender exclusions (highly recommended for performance!)" ExecWait '"$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 `, sh: (config) => `#!/bin/sh basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") "$basedir/../client/bin/${config.bin}.cmd" "$@" ret=$? exit $ret `, /* eslint-enable no-useless-escape */ }; class PackWin extends core_1.Command { static description = `You need to have 7zip, nsis (makensis), and grep installed on your machine in order to run this command. This command will produce unsigned installers unless you supply WINDOWS_SIGNING_PASS (prefixed with the name of your executable, e.g. OCLIF_WINDOWS_SIGNING_PASS) in the environment and have set the windows.name and windows.keypath properties in your package.json's oclif property. Add a pretarball script to your package.json if you need to run any scripts before the tarball is created.`; static flags = { '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, }), 'defender-exclusion': core_1.Flags.option({ options: ['checked', 'unchecked', 'hidden'], })({ default: 'checked', description: 'There is no way to set a hidden checkbox with "true" as a default...the user can always allow full security', summary: `Set to "checked" or "unchecked" to set the default value for the checkbox. Set to "hidden" to hide the option (will let defender do its thing).`, }), 'prune-lockfiles': core_1.Flags.boolean({ description: 'remove lockfiles in the tarball.', exclusive: ['tarball'] }), root: core_1.Flags.string({ char: 'r', default: '.', description: 'Path to oclif CLI root.', required: true, }), sha: core_1.Flags.string({ description: '7-digit short git commit SHA (defaults to current checked out commit).', required: false, }), tarball: core_1.Flags.string({ char: 't', description: 'Optionally specify a path to a tarball already generated by NPM.', exclusive: ['prune-lockfiles'], required: false, }), targets: core_1.Flags.string({ description: 'Comma-separated targets to pack (e.g.: win32-x64,win32-x86,win32-arm64).', }), }; static summary = 'Create windows installer from oclif CLI'; async run() { await this.checkForNSIS(); const { flags } = await this.parse(PackWin); const buildConfig = await Tarballs.buildConfig(flags.root, { sha: flags?.sha, targets: flags?.targets?.split(',') }); const { config } = buildConfig; const nsisCustomization = config.nsisCustomization ? (0, node_fs_1.readFileSync)(config.nsisCustomization, 'utf8') : ''; const arches = buildConfig.targets.filter((t) => t.platform === 'win32').map((t) => t.arch); await Tarballs.build(buildConfig, { pack: false, parallel: true, platform: 'win32', pruneLockfiles: flags['prune-lockfiles'], tarball: flags.tarball, }); await Promise.all(arches.map(async (arch) => { const installerBase = node_path_1.default.join(buildConfig.tmp, `windows-${arch}-installer`); await (0, promises_1.rm)(installerBase, { force: true, recursive: true }); await (0, promises_1.mkdir)(node_path_1.default.join(installerBase, 'bin'), { recursive: true }); await Promise.all([ (0, promises_1.writeFile)(node_path_1.default.join(installerBase, 'bin', `${config.bin}.cmd`), scripts.cmd(config, undefined, buildConfig.nodeOptions)), (0, promises_1.writeFile)(node_path_1.default.join(installerBase, 'bin', `${config.bin}`), scripts.sh(config)), (0, promises_1.writeFile)(node_path_1.default.join(installerBase, `${config.bin}.nsi`), scripts.nsis({ arch, config, customization: nsisCustomization, // hiding it also unchecks it defenderOptional: flags['defender-exclusion'] === 'hidden' || flags['defender-exclusion'] === 'unchecked', hideDefenderOption: flags['defender-exclusion'] === 'hidden', })), ...(config.binAliases ? config.binAliases.flatMap((alias) => // write duplicate files for windows aliases // this avoids mklink which can require admin privileges which not everyone has [ (0, promises_1.writeFile)(node_path_1.default.join(installerBase, 'bin', `${alias}.cmd`), scripts.cmd(config)), (0, promises_1.writeFile)(node_path_1.default.join(installerBase, 'bin', `${alias}`), scripts.sh(config)), ]) : []), ...(flags['additional-cli'] ? [ (0, promises_1.writeFile)(node_path_1.default.join(installerBase, 'bin', `${flags['additional-cli']}.cmd`), scripts.cmd(config, flags['additional-cli'])), (0, promises_1.writeFile)(node_path_1.default.join(installerBase, 'bin', `${flags['additional-cli']}`), scripts.sh({ bin: flags['additional-cli'] })), ] : []), ]); await (0, fs_extra_1.move)(buildConfig.workspace({ arch, platform: 'win32' }), node_path_1.default.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', { arch, bin: config.bin, sha: buildConfig.gitSha, version: config.version, }); const o = buildConfig.dist(`win32/${templateKey}`); await (0, fs_extra_1.move)(node_path_1.default.join(installerBase, 'installer.exe'), o); const { windows } = config.pjson.oclif; 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) { const { code } = error; if (code === 1) return; if (code === 127) this.error('install makensis'); else throw error; } } } exports.default = PackWin; async function signWindows(o, arch, config, windows) { if (!windows) { throw new Error('windows not set in oclif configuration'); } const buildLocationUnsigned = o.replace(`${arch}.exe`, `${arch}-unsigned.exe`); await (0, fs_extra_1.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`); } const args = [ '-pkcs12', windows.keypath, '-pass', `"${pass}"`, '-n', `"${windows.name}"`, '-i', windows.homepage || config.pjson.homepage, '-t', 'http://timestamp.digicert.com', '-h', 'sha512', '-in', buildLocationUnsigned, '-out', o, ]; await exec(`osslsigncode sign ${args.join(' ')}`); }