#!/usr/bin/env zx const execSync = require('child_process').execSync // Log symbols const figuresDefault = { bullet: '●', circle: '◯', cross: '✖', lozenge: '◆', play: '▶', pointer: '❯', square: '◼', star: '★', tick: '✔' } const figuresFallback = { bullet: '■', circle: '□', cross: '×', lozenge: '♦', play: '►', pointer: '>', square: '■', star: '✶', tick: '√' } function isUnicodeSupported() { if (process.platform !== 'win32') { // Linux console (kernel) return process.env.TERM !== 'linux' } return ( Boolean(process.env.CI) || // Windows Terminal Boolean(process.env.WT_SESSION) || // ConEmu and cmder process.env.ConEmuTask === '{cmd::Cmder}' || process.env.TERM_PROGRAM === 'vscode' || process.env.TERM === 'xterm-256color' || process.env.TERM === 'alacritty' ) } const figures = isUnicodeSupported() ? figuresDefault : figuresFallback function log(type, label, msg) { let icon, message let fittedLabel = label while(fittedLabel.length < 14) { fittedLabel = fittedLabel.length % 2 === 1 ? fittedLabel + ' ' : ' ' + fittedLabel } if (type === 'info') { icon = `${chalk.cyanBright(figures.pointer)} ${chalk.bold.white.bgCyan(' ' + fittedLabel + ' ')}` message = wrapMessage(msg) } else if (type === 'star') { icon = `${chalk.yellow(figures.star)} ${chalk.bold.black.bgYellow(' ' + fittedLabel + ' ')}` message = wrapMessage(msg) } else if (type === 'success') { icon = `${chalk.greenBright(figures.play)} ${chalk.bold.white.bgGreenBright(' ' + fittedLabel + ' ')}` message = wrapMessage(msg) } else if (type === 'warn') { icon = `${chalk.yellowBright(figures.lozenge)} ${chalk.bold.black.bgYellowBright(' WARNING ')}` message = chalk.yellowBright(wrapMessage(msg)) } else if (type === 'error') { icon = `${chalk.redBright(figures.cross)} ${chalk.black.bold.bgRedBright(' ERROR ')}` message = chalk.redBright(wrapMessage(msg)) } const now = new Date() const outputMessage = `${icon} ${message} (${now})` console.log(outputMessage) } function locations(substring,string){ var a=[],i=-1; while((i=string.indexOf(substring,i+1)) >= 0) a.push(i); return a; } function wrapMessage(msg) { const indexes = locations('`', msg) if (indexes.length > 3) { return msg.substring(0, indexes[0]) + chalk.bold.black.bgGray(' ' + msg.substring(indexes[0] + 1, indexes[1] + 1 - indexes[0]) + ' ') + msg.substring(indexes[1] + 1 - indexes[0]) + ' ' } else { return msg } } function runCommand(spinnerTitle, command) { // execSync(command.includes('sudo') ? `sudo "$(which gum)" spin --spinner dot --title "${spinnerTitle}" -- ${command}` : `gum spin --spinner dot --title "${spinnerTitle}" -- ${command}`, { log('info', 'CMD', spinnerTitle) console.log(command) execSync(command, { stdio: 'inherit', shell: true, // Timeout of 140m timeout: 1000 * 60 * 140 }) } async function runSilentCommand(command) { execSync(`${command}`, { stdio: 'inherit', shell: true }) } function fileExists(pathToFile) { return fs.existsSync(pathToFile) } function dirExists(pathToDir) { return fs.existsSync(pathToDir) } let installData let installOrders = {} let installMeta = {} let binLinkRan = false let chezmoiData = [] let installOrdersPre = [] let installOrdersPost = [] let installOrdersService = [] let installOrdersGroups = [] let installOrdersPorts = [] let installOrdersPlugins = [] let installOrdersBinLink = [] let brewUpdated, osType, osID, snapRefreshed // Register the OS platform type const osPlatformData = os.platform() const osPlatform = osPlatformData === 'win32' ? 'windows' : osPlatformData // Download the installation map async function downloadInstallData() { const response = await fetch('https://github.com/megabyte-labs/install.doctor/raw/master/software.yml') if (response.ok) { log('info', 'Catalog Download', `Received ok response from download`) const text = await response.text() log('info', 'Catalog Download', `Parsing software.yml`) return YAML.parse(text, { maxAliasCount: -1 }) } else { log('error', 'Catalog Download', `Failed to download the installation map`) log('info', 'Catalog Download', `Falling back to local version of software.yml`) const text = fs.readFileSync(process.env.HOME + '/.local/share/chezmoi/software.yml').toString() log('info', 'Catalog Download', `Parsing local software.yml file`) return YAML.parse(text, { maxAliasCount: -1 }) } } // Download the installation map async function getChezmoiData() { const text = fs.readFileSync(process.env.HOME + '/.local/share/chezmoi/home/.chezmoidata.yaml').toString() return YAML.parse(text, { maxAliasCount: -1 }) } // Creates the installOrders object which maps package managers to arrays of packages to install let generateInstallOrderCount = 0 async function generateInstallOrders(pkgsToInstall) { const installerPreference = await OSTypeInstallerKey() const preferenceOrder = installData.installerPreference[installerPreference] const logStage = 'Install Orders' const packagesToInstall = pkgsToInstall const softwarePackages = installData.softwarePackages if (generateInstallOrderCount === 0) { log('info', logStage, `Installer preference category detected as ${installerPreference}`) log('info', logStage, `Preference order acquired:`) console.log('Preference order:', preferenceOrder) } generateInstallOrderCount++ log('info', logStage, `New packages discovered for processing: ${pkgsToInstall} (${pkgsToInstall.length} items)`) pkgFor: for (let pkg of packagesToInstall) { let packageKey if (softwarePackages[pkg + ':' + osID]) { packageKey = pkg + ':' + osID } else if (softwarePackages[pkg + ':' + osType]) { packageKey = pkg + ':' + osType } else if (softwarePackages[pkg]) { packageKey = pkg } else { log('warn', logStage, `The package \`${pkg}\` was not found in the installation map`) process.env.DEBUG === 'true' && console.log('softwarePackages:', softwarePackages) console.log('pkg:', pkg) continue } let comparesRemaining = preferenceOrder.length for (let preference of preferenceOrder) { comparesRemaining-- let currentSelector, doubleScoped, scopedPkgManager, scopedSystem, normalCheck if ( softwarePackages[packageKey][preference + ':' + osID] || softwarePackages[packageKey][preference + ':' + osType] || softwarePackages[packageKey][preference] ) { // Handle the _when attribute currentSelector = 'when' doubleScoped = softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID] || softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference] || softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType] || softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference] scopedPkgManager = softwarePackages[packageKey]['_' + currentSelector + ':' + preference] scopedSystem = softwarePackages[packageKey]['_' + currentSelector + ':' + osID] || softwarePackages[packageKey]['_' + currentSelector + ':' + osType] normalCheck = softwarePackages[packageKey]['_' + currentSelector] if (doubleScoped) { try { await runSilentCommand(doubleScoped) } catch (e) { let pref if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID]) { pref = preference + ':' + osID } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType]) { pref = preference + ':' + osType } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference]) { pref = osID + ':' + preference } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference]) { pref = osType + ':' + preference } process.env.DEBUG === 'true' && log('info', 'Filter', `${pkg} is being skipped because of the _when:${pref} condition`) processPluginOrders(pkg) continue pkgFor } } else if (scopedPkgManager) { try { await runSilentCommand(scopedPkgManager) } catch (e) { const pref = preference process.env.DEBUG === 'true' && log('info', 'Filter', `${pkg} is being skipped because of the _when:${pref} condition`) processPluginOrders(pkg) continue pkgFor } } else if (scopedSystem) { try { await runSilentCommand(scopedSystem) } catch (e) { let pref if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID]) { pref = osID } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType]) { pref = osType } process.env.DEBUG === 'true' && log('info', 'Filter', `${pkg} is being skipped because of the _when:${pref} condition`) processPluginOrders(pkg) continue pkgFor } } else if (normalCheck) { try { await runSilentCommand(normalCheck) } catch (e) { process.env.DEBUG === 'true' && log('info', 'Filter', `${pkg} is being skipped because of the _when condition`) processPluginOrders(pkg) continue pkgFor } } // Handle the _bin attribute currentSelector = 'bin' doubleScoped = softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID] || softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference] || softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType] || softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference] scopedPkgManager = softwarePackages[packageKey]['_' + currentSelector + ':' + preference] scopedSystem = softwarePackages[packageKey]['_' + currentSelector + ':' + osID] || softwarePackages[packageKey]['_' + currentSelector + ':' + osType] normalCheck = softwarePackages[packageKey]['_' + currentSelector] if (doubleScoped) { const bin = typeof doubleScoped === 'string' ? which.sync(doubleScoped, { nothrow: true }) : doubleScoped.map((x) => which.sync(x, { nothrow: true })).every((y) => !!y) if (bin) { let pref if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID]) { pref = preference + ':' + osID } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType]) { pref = preference + ':' + osType } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference]) { pref = osID + ':' + preference } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference]) { pref = osType + ':' + preference } process.env.DEBUG === 'true' && log('info', 'Filter', `${bin} already in PATH (via _bin:${pref})`) processPluginOrders(pkg) continue pkgFor } else { if (preference === 'cask' || preference === 'flatpak') { installOrdersBinLink.push({ package: packageKey, bin: doubleScoped, preference }) } } } else if (scopedPkgManager) { const bin = typeof scopedPkgManager === 'string' ? which.sync(scopedPkgManager, { nothrow: true }) : scopedPkgManager.map((x) => which.sync(x, { nothrow: true })).every((y) => !!y) if (bin) { const pref = preference process.env.DEBUG === 'true' && log('info', 'Filter', `${bin} already in PATH (via _bin:${pref})`) processPluginOrders(pkg) continue pkgFor } else { if (preference === 'cask' || preference === 'flatpak') { installOrdersBinLink.push({ package: packageKey, bin: scopedPkgManager, preference }) } } } else if (scopedSystem) { const bin = typeof scopedSystem === 'string' ? which.sync(scopedSystem, { nothrow: true }) : scopedSystem.map((x) => which.sync(x, { nothrow: true })).every((y) => !!y) if (bin) { let pref if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID]) { pref = osID } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType]) { pref = osType } process.env.DEBUG === 'true' && log('info', 'Filter', `${bin} already in PATH (via _bin:${pref})`) processPluginOrders(pkg) continue pkgFor } else { if (preference === 'cask' || preference === 'flatpak') { installOrdersBinLink.push({ package: packageKey, bin: scopedSystem, preference }) } } } else if (normalCheck) { const bin = typeof normalCheck === 'string' ? which.sync(normalCheck, { nothrow: true }) : normalCheck.map((x) => which.sync(x, { nothrow: true })).every((y) => !!y) if (bin) { process.env.DEBUG === 'true' && log('info', 'Filter', `${bin} already in PATH (via _bin)`) processPluginOrders(pkg) continue pkgFor } else { if (preference === 'cask' || preference === 'flatpak') { installOrdersBinLink.push({ package: packageKey, bin: normalCheck, preference }) } } } // Handle the _app definition const appName = softwarePackages[packageKey]['_app'] if (appName) { if(fileExists(`/Applications/${appName}`) || fileExists(`${process.env.HOME}/Applications/${appName}`)) { processPluginOrders(pkg) continue pkgFor } } // Handle the _deps attribute currentSelector = 'deps' doubleScoped = softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID] || softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference] || softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType] || softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference] scopedPkgManager = softwarePackages[packageKey]['_' + currentSelector + ':' + preference] scopedSystem = softwarePackages[packageKey]['_' + currentSelector + ':' + osID] || softwarePackages[packageKey]['_' + currentSelector + ':' + osType] normalCheck = softwarePackages[packageKey]['_' + currentSelector] const dependenciesTag = 'Dependencies' if (doubleScoped) { let pref if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID]) { pref = '_deps:' + preference + ':' + osID log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`) await generateInstallOrders(softwarePackages[packageKey][pref]) } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType]) { pref = '_deps:' + preference + ':' + osType log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`) await generateInstallOrders(softwarePackages[packageKey][pref]) } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference]) { pref = '_deps:' + osID + ':' + preference log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`) await generateInstallOrders(softwarePackages[packageKey][pref]) } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference]) { pref = '_deps:' + osType + ':' + preference log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`) await generateInstallOrders(softwarePackages[packageKey][pref]) } } else if (scopedPkgManager) { const pref = '_deps:' + preference log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`) await generateInstallOrders(softwarePackages[packageKey][pref]) } else if (scopedSystem) { let pref if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID]) { pref = '_deps:' + osID log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`) await generateInstallOrders(softwarePackages[packageKey][pref]) } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType]) { pref = '_deps:' + osType log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`) await generateInstallOrders(softwarePackages[packageKey][pref]) } } else if (normalCheck) { log('info', dependenciesTag, `Installing dependencies for ${packageKey}.deps`) await generateInstallOrders(softwarePackages[packageKey]['_deps']) } if (softwarePackages[packageKey][preference + ':' + osID]) { await updateInstallMaps( preference, softwarePackages[packageKey], preference + ':' + osID, pkg, packageKey, softwarePackages ) break } else if (softwarePackages[packageKey][preference + ':' + osType]) { await updateInstallMaps( preference, softwarePackages[packageKey], preference + ':' + osType, pkg, packageKey, softwarePackages ) break } else if (softwarePackages[packageKey][preference]) { await updateInstallMaps( preference, softwarePackages[packageKey], preference, pkg, packageKey, softwarePackages ) break } } else { if (!comparesRemaining) { log('info', 'No Match', `There was no match found for ${pkg} - it may be an OS-specific package`) } } } } if (generateInstallOrderCount === 1) { return installOrders } else { generateInstallOrderCount-- } } function processPluginOrders(pkg) { const pluginMap = chezmoiData && chezmoiData.softwarePlugins && chezmoiData.softwarePlugins[pkg] if (pluginMap) { if (pluginMap.cmd && pluginMap.plugins) { installOrdersPlugins.push({ package: pkg, cmd: pluginMap.cmd, plugins: pluginMap.plugins }) } } } // Update install, pre-hook, and post-hook objects async function updateInstallMaps(preference, packages, scopedPreference, pkg, packageKey, softwarePackages) { const preHook = getHook(packages, 'pre', scopedPreference, preference) if (preHook) { installOrdersPre = installOrdersPre.concat(typeof preHook === 'string' ? [preHook] : preHook) } const postHook = getHook(packages, 'post', scopedPreference, preference) if (postHook) { installOrdersPost = installOrdersPost.concat(typeof postHook === 'string' ? [postHook] : postHook) } const serviceHook = getHook(packages, 'service', scopedPreference, preference) const serviceEnabledHook = getHook(packages, 'serviceEnabled', scopedPreference) if (serviceHook && serviceEnabledHook) { installOrdersService = installOrdersService.concat(typeof serviceHook === 'string' ? [serviceHook] : serviceHook) } const groupsHook = getHook(packages, 'groups', scopedPreference, preference) if (groupsHook) { installOrdersGroups = installOrdersGroups.concat(typeof groupsHook === 'string' ? [groupsHook] : groupsHook) } const portsHook = getHook(packages, 'ports', scopedPreference, preference) if (portsHook) { installOrdersPorts = installOrdersPorts.concat(typeof portsHook === 'string' ? [{ packageKey, ports: portsHook }] : { packageKey, ports: portsHook}) } processPluginOrders(pkg) if (!installOrders[preference]) { installOrders[preference] = [] } log('info', 'Match', `Found a match for the package \`${pkg}\` (${packageKey} via ${scopedPreference})`) const newPackages = packages[scopedPreference] const newPkgs = typeof newPackages === 'string' ? [ newPackages ] : newPackages if (typeof newPackages === 'string') { installMeta[newPackages] = { preference, packages, scopedPreference, pkg, packageKey, softwarePackages } } else { for (const dataKey in newPackages) { installMeta[newPackages] = { preference, packages, scopedPreference, pkg, packageKey, softwarePackages } } } if (preference === 'snap' && softwarePackages[pkg]['_snapClassic'] === true) { if (!installOrders['snap-classic']) { installOrders['snap-classic'] = [] } installOrders['snap-classic'] = installOrders['snap-classic'].concat(newPkgs) } else { installOrders[preference] = installOrders[preference].concat(newPkgs) } } // Get pre / post install hooks function getHook(packages, hook, scopedPreference, preference) { const hookLabel = '_' + hook + ':' if (packages[hookLabel + scopedPreference]) { return packages[hookLabel + scopedPreference] } else if (packages[hookLabel + preference]) { return packages[hookLabel + preference] } else if (packages[hookLabel + osID]) { return packages } else if (packages[hookLabel + osType]) { return packages[hookLabel + osType] } else if (packages['_' + hook]) { return packages['_' + hook] } } // Acquire OS type installer key (for the installerPreference data key) async function OSTypeInstallerKey() { try { const apt = which.sync('apt-get', { nothrow: true }) const dnf = which.sync('dnf', { nothrow: true }) const freebsdPkg = which.sync('pkg', { nothrow: true }) const freebsdVersion = which.sync('freebsd-version', { nothrow: true }) const pacman = which.sync('pacman', { nothrow: true }) const yum = which.sync('yum', { nothrow: true }) const zypper = which.sync('zypper', { nothrow: true }) if (apt) { return dirExists('/etc/ubuntu-advantage') ? 'ubuntu' : 'apt' } else if (dnf || yum) { return 'dnf' } else if (pacman) { return 'pacman' } else if (zypper) { return 'zypper' } else if (freebsdPkg && freebsdVersion) { return 'freebsd' } else { return dirExists('/Applications') && dirExists('/Library') ? 'darwin' : 'windows' } } catch (e) { log('error', 'OS Detection', 'There was an error determining the type of operating system') console.error(e) } } // Acquire OS type async function OSType() { return dirExists('/Applications') && dirExists('/Library') ? 'darwin' : (fileExists('/etc/os-release') ? 'linux' : 'windows') } // Acquire release ID (for Linux) async function releaseID() { const ID = await $` if [ -f /etc/os-release ]; then . /etc/os-release echo -n $ID fi ` return ID.stdout } // Post-install hook async function afterInstall(packageManager) { const logStage = 'Post-Install Package Manager' if (packageManager === 'appimage') { } else if (packageManager === 'ansible') { log('info', logStage, `Ensuring temporary passwordless sudo privileges used by Ansible are removed`) const gsed = which.sync('gsed', { nothrow: true }) if (gsed) { await $`sudo gsed -i '/# TEMPORARY FOR INSTALL DOCTOR ANSIBLE/d' /etc/sudoers` } else { await $`sudo sed -i '/# TEMPORARY FOR INSTALL DOCTOR ANSIBLE/d' /etc/sudoers` } } else if (packageManager === 'apk') { } else if (packageManager === 'apt') { try { runCommand('Running apt-get autoclean', `sudo apt-get autoclean`) runCommand('Running apt-get autoremove', `sudo apt-get -y autoremove`) } catch (e) { log('error', logStage, 'Error cleaning up apt-get') } } else if (packageManager === 'basher') { } else if (packageManager === 'binary') { } else if (packageManager === 'brew' || packageManager === 'cask') { log('info', logStage, `Ensuring Homebrew cleanup is run`) await $`brew cleanup` } else if (packageManager === 'cargo') { } else if (packageManager === 'choco') { } else if (packageManager === 'crew') { } else if (packageManager === 'dnf') { } else if (packageManager === 'flatpak') { } else if (packageManager === 'gem') { } else if (packageManager === 'go') { } else if (packageManager === 'nix') { } else if (packageManager === 'npm') { } else if (packageManager === 'pacman') { } else if (packageManager === 'pipx') { } else if (packageManager === 'pkg') { } else if (packageManager === 'port') { } else if (packageManager === 'scoop') { } else if (packageManager === 'script') { } else if (packageManager === 'snap') { } else if (packageManager === 'whalebrew') { } else if (packageManager === 'winget') { } else if (packageManager === 'yay') { } else if (packageManager === 'zypper') { } } async function ensurePackage(dep) { const target = which.sync(dep, { nothrow: true }) if (!target) { if (osType === 'linux') { const apk = which.sync('apk', { nothrow: true }) const apt = which.sync('apt-get', { nothrow: true }) const dnf = which.sync('dnf', { nothrow: true }) const pkg = which.sync('pkg', { nothrow: true }) const yum = which.sync('yum', { nothrow: true }) const pacman = which.sync('pacman', { nothrow: true }) const zypper = which.sync('zypper', { nothrow: true }) if (apk) { await $`sudo apk add ${dep}` } else if (apt) { if (updateDone['apt-get'] !== true) { await beforeInstall('apt-get') } try { log('info', 'apt-get Installation', `Checking if ${dep} is already installed`) runCommand( `Checking if ${dep} is already installed via apt-get`, `dpkg -l ${dep} | grep -E '^ii' > /dev/null` ) log('info', 'Filter', `${pkg} already installed via apt-get`) } catch (e) { runCommand( `Installing ${dep} via apt-get`, `sudo apt-get -o DPkg::Options::=--force-confdef install -y ${dep}` ) log('success', 'Install', `Successfully installed ${pkg} via apt-get`) } } else if (dnf) { if (updateDone['dnf'] !== true) { await beforeInstall('dnf') } try { log('info', 'dnf Installation', `Checking if ${dep} is already installed`) await $`rpm -qa | grep '$'"${dep}-" > /dev/null` } catch (e) { log('info', 'dnf Installation', `Installing ${dep} since it is not already present on the system`) await $`sudo dnf install -y ${dep}` } } else if (yum) { if (updateDone['yum'] !== true) { await beforeInstall('yum') } try { log('info', 'YUM Installation', `Checking if ${dep} is already installed`) await $`rpm -qa | grep '$'"${dep}-" > /dev/null` } catch (e) { log('info', 'YUM Installation', `Installing ${dep} since it is not already present on the system`) await $`sudo yum install -y ${dep}` } } else if (pacman) { if (updateDone['pacman'] !== true) { await beforeInstall('pacman') } try { log('info', 'Pacman Installation', `Checking if ${dep} is already installed`) await $`pacman -Qs ${dep}` } catch (e) { log('info', 'Pacman Installation', `Installing ${dep} since it is not already present on the system`) await $`sudo pacman -Sy ${dep}` } } else if (zypper) { if (updateDone['zypper'] !== true) { await beforeInstall('zypper') } try { log('info', 'Zypper Installation', `Checking if ${dep} is already installed`) await $`rpm -qa | grep '$'"${dep}-" > /dev/null` } catch (e) { log('info', 'Zypper Installation', `Installing ${dep} since it is not already present on the system`) await $`sudo zypper install -y ${dep}` } } else if (pkg) { if (updateDone['pkg'] !== true) { await beforeInstall('pkg') } try { log('info', 'pkg Installation', `Checking if ${dep} is already installed`) await $`pkg info -Ix ${dep} > /dev/null` } catch (e) { log('info', 'pkg Installation', `Installing ${dep} since it is not already present on the system`) await $`sudo pkg install -y ${dep}` } } } else if (osType === 'darwin') { if (updateDone['brew'] !== true) { await beforeInstall('brew') } await $`brew install --quiet ${dep}` } else if (osType === 'windows') { if (updateDone['choco'] !== true) { await beforeInstall('choco') } await `choco install -y ${dep}` } } } // Pre-install hook const updateDone = {} async function beforeInstall(packageManager) { updateDone[packageManager] = true const logStage = 'Pre-Install Package Manager' if (packageManager === 'appimage') { if (!fileExists(`${process.env.HOME}/Applications`)) { runSilentCommand(`mkdir -p "${process.env.HOME}/Applications"`) } } else if (packageManager === 'ansible') { log('info', logStage, `Temporarily enabling passwordless sudo for Ansible role installations`) await $`echo "$(whoami) ALL=(ALL:ALL) NOPASSWD: ALL # TEMPORARY FOR ANSIBLE INSTALL DOCTOR" | sudo tee -a /etc/sudoers` log('info', logStage, 'Running Ansible setup task so facts are cached') const unbuffer = which.sync('unbuffer', { nothrow: true }) let unbufferPrefix = '' if (unbuffer) { unbufferPrefix = 'unbuffer' } if (osPlatform === 'darwin' || osPlatform === 'linux' || osPlatform === 'windows') { const capitalOsPlatform = osPlatform.charAt(0).toUpperCase() + osPlatform.slice(1) await $`ANSIBLE_CONFIG=${process.env.HOME}/.local/share/ansible/ansible.cfg ${unbufferPrefix} ansible 127.0.0.1 -e '{ ansible_connection: "local", ansible_become_user: "${process.env.USER}", ansible_user: "${process.env.USER}", ansible_family: "${capitalOsPlatform}", install_homebrew: False }' -m setup` } else { log('warn', 'Ansible', 'Unsupported platform - ' + osPlatform) } } else if (packageManager === 'apk') { await $`sudo apk update` } else if (packageManager === 'apt') { runCommand('Running apt-get update / upgrade', `sudo apt-get update && sudo apt-get upgrade -y`) } else if (packageManager === 'basher') { } else if (packageManager === 'binary') { } else if (packageManager === 'brew' || packageManager === 'cask') { if (!brewUpdated) { brewUpdated = true try { runCommand('Running brew update / upgrade', `brew update && brew upgrade --cask && brew upgrade`) runCommand('Running brew update', `brew update`) runCommand('Running brew upgrade', `brew upgrade`) if (osType === 'darwin'){ runCommand('Running brew upgrade (Casks)', `brew upgrade --cask`) } } catch (e) { console.log(e) log('error', 'Homebrew', 'Failed running brew update / upgrade') log('info', 'Homebrew', 'Running brew tap --repair and trying again') try { runCommand('Repairing taps and retrying brew update / upgrade', 'export HOMEBREW_TEMP=/tmp && sudo rm -rf $(brew --cache) && brew tap --repair && brew update && brew upgrade --force --greedy && [[ $OSTYPE != "darwin"* ]] && brew upgrade --cask --greedy') } catch (e) { console.log(e) log('error', 'Homebrew', 'Failed both attempts to run brew update / upgrade') } } } } else if (packageManager === 'cargo') { } else if (packageManager === 'choco') { } else if (packageManager === 'crew') { await $`crew update` } else if (packageManager === 'dnf') { const dnf = which.sync('dnf', { nothrow: true }) const yum = which.sync('yum', { nothrow: true }) if (dnf) { runCommand('Running dnf update', `sudo dnf update -y`) } else if (yum) { runCommand('Running yum update', `sudo yum update -y`) } } else if (packageManager === 'flatpak') { // TODO - figure out why CentOS is failing to update // and then switch command below for `sudo flatpak update -y` runCommand('Running flatpak update', `. /etc/os-release; if [ "$ID" != "centos" ]; then sudo flatpak update -y; else echo "Skipping Flatpak update on CentOS"; fi`) } else if (packageManager === 'gem') { } else if (packageManager === 'go') { } else if (packageManager === 'nix') { runCommand('Running nix-channel --update', `nix-channel --update`) } else if (packageManager === 'npm') { } else if (packageManager === 'pacman') { runCommand('Running pacman update', `sudo pacman -Syu`) } else if (packageManager === 'pipx') { } else if (packageManager === 'pkg') { await $`sudo pkg upgrade` } else if (packageManager === 'port') { const port = which.sync('port', { nothrow: true }) if (port) { runCommand('Running port sync', `sudo port sync`) } else { log('error', 'Port Not Installed', 'Skipping sudo port sync step because port is not installed') } } else if (packageManager === 'scoop') { runCommand('Running scoop update', `scoop update`) } else if (packageManager === 'snap' || packageManager === 'snap-classic') { if (!snapRefreshed) { snapRefreshed = true runCommand('Ensuring snap is refreshed', `sudo snap refresh`) } } else if (packageManager === 'whalebrew') { if (osType === 'darwin') { const docker = which.sync('docker', { nothrow: true }) if (!docker) { await $`brew install --cask --no-quarantine --quiet docker` } try { await $`sudo -c 'docker run --rm hello-world' - ${process.env.USER}` } catch (e) { log('warn', logStage, `The command \`docker run --rm hello-world\` failed`) try { log( 'info', logStage, 'Attempting to open `/Applications/Docker.app` (Docker Desktop for macOS). This should take about 30 seconds.' ) const promises = [$`test -d /Applications/Docker.app`, $`rm -f ~/.docker && mkdir -p ~/.config/docker && open --background -a Docker --args --accept-license --unattended`] await Promise.all(promises) const gum = which.sync('gum', { nothrow: true }) if (gum) { execSync('gum spin --spinner dot --title "Waiting for Docker Desktop to start up.." -- sleep 30', { stdio: 'inherit', shell: true }) } else { await $`sleep 30` } } catch (e) { log('warn', logStage, `Docker Desktop appears to not be installed!`) } } } } else if (packageManager === 'winget') { runCommand('Running winget source update', `winget source update`) } else if (packageManager === 'yay') { } else if (packageManager === 'zypper') { runCommand('Running zypper update', `sudo zypper update`) } } async function ensureInstalled(bin, callback) { const logStage = 'Package Manager Install' const installed = which.sync(bin, { nothrow: true }) if (installed) { log('info', logStage, `\`${bin}\` is available`) } else { log('warn', logStage, `\`${bin}\` is not installed!`) if (callback) { await callback } else { log('error', logStage, `There does not appear to be an installation method available for \`${bin}\``) } } } async function ensurePackageManagerAnsible() { await $`pipx install ansible-core` if (osType === 'darwin') { await $`pipx inject ansible-core PyObjC PyObjC-core` } await $`pipx inject ansible-core docker lxml netaddr pexpect python-vagrant pywinrm requests-credssp watchdog` await $`mkdir -p "$HOME/.cache/megabyte-labs"` await $`touch "$HOME/.cache/megabyte-labs/ansible-installed"` log('info', 'Package Manager Install', `Ansible and its supporting packages are now installed via pipx`) } // Ensure the package manager is available let packageManagerInstalled = {} async function ensurePackageManager(packageManager) { const logStage = 'Pre-Reqs' log('info', logStage, `Ensuring \`${packageManager}\` is set up`) if (packageManagerInstalled[packageManager]) { return } else { packageManagerInstalled[packageManager] = true } if (packageManager === 'ansible') { await ensurePackageManager('pipx') } if ( packageManager === 'gem' || packageManager === 'go' || packageManager === 'npm' || packageManager === 'pipx' || packageManager === 'whalebrew' ) { await ensurePackageManager('brew') } if (packageManager === 'appimage') { const zap = which.sync('zap', { nothrow: true }) if (!zap) { log('info', 'Zap Installation', 'Installing Zap to handle AppImage installation') await ensurePackage('curl') await $`sudo curl -sSL --output /usr/local/bin/zap https://github.com/srevinsaju/zap/releases/download/continuous/zap-amd64` await $`sudo chmod +x /usr/local/bin/zap` } } else if (packageManager === 'ansible') { try { await $`test -f "$HOME/.cache/megabyte-labs/ansible-installed"` const ansible = which.sync('ansible', { nothrow: true }) if (ansible) { log('info', logStage, `\`ansible\` and its supporting packages appear to be installed`) } else { await ensurePackageManagerAnsible() } } catch (e) { await ensurePackageManagerAnsible() } } else if (packageManager === 'apk') { await ensureInstalled('apk', false) } else if (packageManager === 'apt') { await ensureInstalled('apt', false) } else if (packageManager === 'basher') { await ensureInstalled( 'basher', $` # TODO echo "Bash script that installs basher here" ` ) } else if (packageManager === 'binary') { await ensurePackage('curl') } else if (packageManager === 'bpkg') { await ensureInstalled( 'bpkg', $` # TODO echo "Bash script that installs bpkg here" ` ) } else if (packageManager === 'brew' || packageManager === 'cask') { const brew = which.sync('brew', { nothrow: true }) if (!brew) { await ensureInstalled( 'brew', $` if command -v sudo > /dev/null && sudo -n true; then echo | bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" sudo chmod -R g-w "$(brew --prefix)/share" else log('info', logStage, 'Homebrew is not installed. Password may be required.') bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || BREW_EXIT_CODE="$?" sudo chmod -R g-w "$(brew --prefix)/share" if [ -n "$BREW_EXIT_CODE" ]; then if command -v brew > /dev/null; then log('warn', logStage, 'Homebrew was installed but part of the installation failed. Attempting to fix..') BREW_DIRS="share/man share/doc share/zsh/site-functions etc/bash_completion.d" for BREW_DIR in $BREW_DIRS; do if [ -d "$(brew --prefix)/$BREW_DIR" ]; then sudo chown -R "$(whoami)" "$(brew --prefix)/$BREW_DIR" fi done brew update --force --quiet fi fi fi ` ) } } else if (packageManager === 'cargo') { const cargo = which.sync('cargo', { nothrow: true }) if (!cargo) { if (osType === 'darwin') { const rustup = which.sync('rustup-init', { nothrow: true }) if (!rustup) { await $`brew install --quiet rustup` } await $`rustup-init -y` } else if (osType === 'windows') { } else { await ensurePackage('cargo') } } } else if (packageManager === 'choco') { await ensureInstalled( 'choco', $` powershell "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))" ` ) } else if (packageManager === 'crew') { await ensureInstalled( 'crew', $` # TODO Bash script that installs crew here # Source: https://github.com/chromebrew/chromebrew curl -Ls git.io/vddgY | bash ` ) } else if (packageManager === 'dnf') { const dnf = which.sync('dnf', { nothrow: true }) const yum = which.sync('yum', { nothrow: true }) if (dnf) { log('info', logStage, `\`dnf\` is available`) } else if (yum) { log('info', logStage, `\`yum\` is available`) } else { log('error', logStage, `Both \`dnf\` and \`yum\` are not available`) } } else if (packageManager === 'flatpak') { const flatpak = which.sync('flatpak', { nothrow: true }) if (flatpak) { log('info', logStage, `\`flatpak\` is available`) } else { const apk = which.sync('apk', { nothrow: true }) const apt = which.sync('apt-get', { nothrow: true }) const dnf = which.sync('dnf', { nothrow: true }) const yum = which.sync('yum', { nothrow: true }) const pacman = which.sync('pacman', { nothrow: true }) const zypper = which.sync('zypper', { nothrow: true }) if (apk) { runCommand('Installing flatpak via apk', 'sudo apk add flatpak') } else if (apt) { runCommand('Installing flatpak via apt-get', 'sudo apt-get install -y flatpak') if (fileExists('/usr/bin/gnome-shell')) { runCommand( 'Installing gnome-software-plugin-flatpak via apt-get', 'sudo apt-get install -y gnome-software-plugin-flatpak' ) } if (fileExists('/usr/bin/plasmashell')) { runCommand('Installing plasmashell via apt-get', 'sudo apt-get install -y plasmashell') } } else if (dnf) { await $`sudo dnf install -y flatpak` } else if (yum) { await $`sudo yum install -y flatpak` } else if (pacman) { await $`sudo pacman -Sy flatpak` } else if (zypper) { await $`sudo zypper install -y flatpak` } log('info', logStage, `\`flatpak\` was installed. It may require a reboot to function correctly.`) } const flatpakPost = which.sync('flatpak', { nothrow: true }) if (flatpakPost) { await $`sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo` } else { log('error', logStage, `\`flatpak\` failed to install!`) } } else if (packageManager === 'gem') { const gem = which.sync('gem', { nothrow: true }) if (!gem) { await ensureInstalled('gem', $`brew install --quiet ruby`) } } else if (packageManager === 'go') { const go = which.sync('go', { nothrow: true }) if (!go) { await ensureInstalled('go', $`brew install --quiet go`) } } else if (packageManager === 'nix') { await ensureInstalled( 'nix', $` if [ -d /Applications ] && [ -d /Library ]; then sh <(curl -L https://nixos.org/nix/install) else sh <(curl -L https://nixos.org/nix/install) --daemon fi ` ) } else if (packageManager === 'npm') { const npm = which.sync('npm', { nothrow: true }) const node = which.sync('node', { nothrow: true }) const volta = which.sync('volta', { nothrow: true }) if (npm && node && volta) { log('info', logStage, `\`npm\`, \`node\`, and \`volta\` are available`) } else { if (!volta) { await $`brew install --quiet volta` } await $` export VOLTA_HOME="\${XDG_DATA_HOME:-$HOME/.local/share}/volta" export PATH="$VOLTA_HOME/bin:$PATH" volta setup volta install node ` } log('info', logStage, 'Ensuring Volt has Node.js runtime available') await $`export VOLTA_HOME="\${XDG_DATA_HOME:-$HOME/.local/share}/volta" && export PATH="$VOLTA_HOME/bin:$PATH" && if ! volta list 2>&1 | grep 'runtime node' > /dev/null; then volta install node; fi` } else if (packageManager === 'pacman') { await ensureInstalled('pacman', false) } else if (packageManager === 'pipx') { const pipx = which.sync('pipx', { nothrow: true }) if (!pipx) { await ensureInstalled('pipx', $`brew install --quiet pipx`) await $`pipx ensurepath` } } else if (packageManager === 'pkg') { await ensureInstalled('pkg', false) } else if (packageManager === 'port') { const port = which.sync('port', { nothrow: true }) if (!port) { log('info', logStage, `Installing ${packageManager}`) await ensureInstalled( 'port', $` sudo mkdir -p /opt/mports cd /opt/mports sudo rm -rf macports-base sudo git clone https://github.com/macports/macports-base.git cd macports-base sudo git checkout v2.8.0 sudo bash --noprofile --norc -c './configure --enable-readline && make && make install && make distclean' sudo port selfupdate ` ) log('info', logStage, `${packageManager} is now installed`) } else { log('info', logStage, `\`port\` is available`) } } else if (packageManager === 'scoop') { await ensureInstalled( 'scoop', $` powershell 'Set-ExecutionPolicy RemoteSigned -Scope CurrentUser' powershell 'irm get.scoop.sh | iex ` ) } else if (packageManager === 'snap') { const apk = which.sync('apk', { nothrow: true }) const apt = which.sync('apt-get', { nothrow: true }) const dnf = which.sync('dnf', { nothrow: true }) const yum = which.sync('yum', { nothrow: true }) const pacman = which.sync('pacman', { nothrow: true }) const zypper = which.sync('zypper', { nothrow: true }) if (apt) { if (fileExists('/etc/apt/preferences.d/nosnap.pref')) { $`sudo mv /etc/apt/preferences.d/nosnap.pref /etc/apt/nosnap.pref.bak` } runCommand('Ensuring snapd is installed', `sudo apt-get install -y snapd`) // TODO Following may be required on Kali -> https://snapcraft.io/docs/installing-snap-on-kali // systemctl enable --now snapd apparmor runCommand('Enabling snapd service', `sudo systemctl enable snapd`) runCommand('Starting snapd service', `sudo systemctl start snapd`) } else if (dnf) { runCommand('Ensuring snapd is installed', `sudo dnf install -y snapd`) if (!fileExists('/snap')) { await $`sudo ln -s /var/lib/snapd/snap /snap` } runCommand('Enabling snapd service', `sudo systemctl enable snapd`) runCommand('Starting snapd service', `sudo systemctl start snapd`) } else if (yum) { runCommand('Ensuring snapd is installed', 'sudo yum install -y snapd') await $`sudo systemctl enable --now snapd.socket` if (!fileExists('/snap')) { $`sudo ln -s /var/lib/snapd/snap /snap` } } else if (pacman) { $`if [ -f /etc/arch-release ]; then sudo git clone https://aur.archlinux.org/snapd.git /usr/local/src/snapd && cd /usr/local/src/snapd && sudo makepkg -si; else sudo pacman -S snapd && sudo systemctl enable --now snapd.socket && if [ ! -d /snap ]; then sudo ln -s /var/lib/snapd/snap /snap; fi; fi` } else if (zypper) { // TODO See https://snapcraft.io/docs/installing-snap-on-opensuse await $` echo "TODO - Bash script that installs snap w/ zypper" ` } const snap = which.sync('snap', { nothrow: true }) if (snap) { runCommand('Check info for core snap package', `sudo snap info core`) // Required: https://snapcraft.io/docs/troubleshooting#heading--early runCommand('Ensuring snap is seeded', `sudo snap wait system seed.loaded`) runCommand('Ensuring snap core is installed', `sudo snap install core`) } else { log('warn', logStage, 'Snap installation sequence completed but the snap bin is still not available') } } else if (packageManager === 'script') { } else if (packageManager === 'whalebrew') { await ensureInstalled('whalebrew', $`brew install --quiet whalebrew`) } else if (packageManager === 'winget') { await ensureInstalled( 'winget', $` echo "TODO - Script that installs winget here" ` ) } else if (packageManager === 'yay') { const yay = which.sync('yay', { nothrow: true }) await $`sudo pacman -S --needed base-devel git` await $` if [ -d /usr/local/src ]; then git clone https://aur.archlinux.org/yay.git /usr/local/src/yay cd /usr/local/src/yay makepkg -si fi ` } else if (packageManager === 'zypper') { await ensureInstalled('zypper', false) } } // Installs a list of packages via the specified package manager async function installPackageList(packageManager, packages) { const logStage = 'Package Install' try { if (packageManager === 'appimage') { for (let pkg of packages) { try { if (pkg.substring(0, 4) === 'http') { log('info', 'AppImage Install', `Installing ${pkg} from its URL`) runCommand('Installing ${pkg} via zap', `zap install --select-first -q --from ${pkg}`) } else if ((pkg.match(/\//g) || []).length === 1) { log('info', 'AppImage Install', `Installing ${pkg} from a GitHub Release`) runCommand('Installing ${pkg} via zap', `zap install --select-first -q --github --from ${pkg}`) } else { log('info', 'AppImage Install', `Installing ${pkg} using the AppImage Catalog`) runCommand('Installing ${pkg} via zap', `zap install --select-first -q ${pkg}`) } log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error using Zap to install ${pkg}`) console.error(e) } } log('warn', 'Install', 'Zap installs might fail - this is expected. Waiting on fixes to Zap upstream project') } else if (packageManager === 'ansible') { for (let pkg of packages) { try { const unbuffer = which.sync('unbuffer', { nothrow: true }) let unbufferPrefix = '' if (unbuffer) { unbufferPrefix = 'unbuffer' } const verboseMode = process.env.DEBUG_MODE === 'true' ? 'vv' : '' if (osPlatform === 'darwin' || osPlatform === 'linux' || osPlatform === 'windows') { const capitalOsPlatform = osPlatform.charAt(0).toUpperCase() + osPlatform.slice(1) await $`ANSIBLE_CONFIG=${process.env.HOME}/.local/share/ansible/ansible.cfg ansible 127.0.0.1 -v${verboseMode} -e '{ ansible_connection: "local", ansible_become_user: "root", ansible_user: "${process.env.USER}", ansible_family: "${capitalOsPlatform}", install_homebrew: False }' -m include_role -a name=${pkg}` log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } else { log('warn', 'Ansible', 'Unsupported platform - ' + osPlatform) } } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with Ansible`) } } } else if (packageManager === 'apk') { for (let pkg of packages) { try { runCommand('Installing ${pkg} via apk', `sudo apk add ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with apk`) console.error(e) } } } else if (packageManager === 'apt') { for (let pkg of packages) { try { if (pkg.startsWith('http') && pkg.endsWith('.deb')) { runCommand( `Downloading and installing ${pkg}`, `TMP="$(mktemp)" && curl -sSL ${pkg} -o "$TMP" && sudo dpkg -i "$TMP"` ) } else { runCommand( `Installing ${pkg} via ${packageManager}`, `sudo DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Options::=--force-confdef install -y ${pkg}` ) } log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with apt-get`) console.error(e) } } } else if (packageManager === 'basher') { for (let pkg of packages) { try { await $`basher install ${pkg}` } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with basher`) console.error(e) } } } else if (packageManager === 'binary') { for (let pkg of packages) { try { const bins = installData.softwarePackages.filter((x) => x.appimage === pkg) if (bins && bins[0]) { const binName = bins[0]['_bin'] await $`TMP="$(mktemp)" && curl -sSL ${pkg} > "$TMP" && sudo mv "$TMP" /usr/local/src/${binName} && chmod +x /usr/local/src/${binName}` } } catch (e) { log('error', 'Install', `There was an error installing the binary release for ${pkg}`) console.error(e) } } } else if (packageManager === 'brew') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `brew install --quiet ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with brew`) console.error(e) } } } else if (packageManager === 'cask') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `brew install --cask --no-quarantine --quiet ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with Homebrew Cask`) console.error(e) } } } else if (packageManager === 'cargo') { for (const pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `cargo install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with Cargo`) console.error(e) } } } else if (packageManager === 'choco') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `choco install -y ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with Chocolatey`) console.error(e) } } } else if (packageManager === 'crew') { } else if (packageManager === 'dnf') { const dnf = which.sync('dnf', { nothrow: true }) const yum = which.sync('yum', { nothrow: true }) for (let pkg of packages) { if (dnf) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `sudo dnf install -y ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with dnf`) console.error(e) } } else if (yum) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `sudo yum install -y ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with yum`) console.error(e) } } } } else if (packageManager === 'flatpak') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `sudo flatpak install -y flathub ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with flatpak`) console.error(e) } } } else if (packageManager === 'gem') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `gem install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with gem`) console.error(e) } } } else if (packageManager === 'go') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `go install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with go`) console.error(e) } } } else if (packageManager === 'nix') { } else if (packageManager === 'npm') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `volta install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with volta`) console.error(e) } } } else if (packageManager === 'pacman') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `sudo pacman -Sy --noconfirm --needed ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with pacman`) console.error(e) } } } else if (packageManager === 'pipx') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `pipx install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('warn', 'Install', `There was an error installing ${pkg} with pipx, re-attempting using pip3`) console.error(e) try { runCommand(`Installing ${pkg} via pip3`, `pip3 install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via pip3`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with both pipx and pip3`) console.error(e) } } } } else if (packageManager === 'pkg-darwin') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `TMP="$(mktemp)" && curl -sSL "${pkg}" > "$TMP" && sudo installer -pkg "$TMP" -target /`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with the system installer`) console.error(e) } } } else if (packageManager === 'port') { const port = which.sync('port', { nothrow: true }) if (port) { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `sudo port install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with port`) console.error(e) } } } else { log( 'error', 'Port Not Installed', `Unable to install with port because it is not installed. Skipping installation of ${packages}` ) } } else if (packageManager === 'scoop') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `scoop install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with scoop`) console.error(e) } } } else if (packageManager === 'snap') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `sudo snap install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with snap`) console.error(e) } } } else if (packageManager === 'script') { for (let pkg of packages) { try { await $`bash -c ${pkg}` } catch (e) { log('error', 'Install', `There was an error running the script installation method for ${pkg}`) console.error(e) } } } else if (packageManager === 'snap-classic') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `sudo snap install --classic ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with snap in classic mode`) console.error(e) } } } else if (packageManager === 'whalebrew') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `whalebrew install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with whalebrew`) console.error(e) } } } else if (packageManager === 'winget') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `winget install ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with winget`) console.error(e) } } } else if (packageManager === 'yay') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `yay -Sy --noconfirm --needed ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with yay`) console.error(e) } } } else if (packageManager === 'zypper') { for (let pkg of packages) { try { runCommand(`Installing ${pkg} via ${packageManager}`, `sudo zypper install -y ${pkg}`) log('success', 'Install', `${pkg} successfully installed via ${packageManager}`) } catch (e) { log('error', 'Install', `There was an error installing ${pkg} with zypper`) console.error(e) } } } } catch (e) { log('error', logStage, `Possibly encountered an error while installing via \`${packageManager}\``) log('info', logStage, `Proceeding with the installation..`) } } async function addUserGroup(group) { const logStage = 'Users / Groups' log('info', logStage, `Ensuring the ${group} group / user is added`) runCommand(`Creating the ${group} user / group`, `sudo "${process.env.HOME}/.local/bin/add-usergroup" "${process.env.USER}" "${group}"`) } /** * Adds the rules specified in the `_ports` key of each entry in the `software.yml` file. * * @param rule {packageKey: string, ports: any} Firewall rule in the form of 8888/tcp or 9999/udp. Can also be the XML file name stored in ~/.config/firewall/etc/firewalld/services. */ async function addFirewallRule(rule) { try { const logStage = 'Firewall' const packageName = installData.softwarePackages[rule.packageKey] && installData.softwarePackages[rule.packageKey]._name const packageDesc = installData.softwarePackages[rule.packageKey] && installData.softwarePackages[rule.packageKey]._desc log('info', logStage, `Ensuring the ${rule.packageKey} rule is added since the _ports key is defined`) if (osType === 'linux') { const firewallCmd = which.sync('firewall-cmd', { nothrow: true }) // const ufw = which.sync('ufw', { nothrow: true }) if (firewallCmd) { const serviceFile = `${process.env.HOME}/.config/firewall/etc/firewalld/services/${rule.packageKey}.xml` if (fileExists(serviceFile)) { log('info', logStage, `Service file found at ${serviceFile} - using it to apply firewall-cmd configuration`) runCommand(`Copying over ${serviceFile} file to /etc/firewalld/services`, `sudo cp -f "${serviceFile}" "/etc/firewalld/services/${rule.packageKey}.xml"`) runCommand(`Adding the ${rule.packageKey} firewall-cmd service`, `sudo firewall-cmd --add-service=${rule.packageKey} --permanent`) } else { if (typeof rule.ports === 'string') { runCommand(`Adding the ${rule.packageKey} ${rule.ports} rule to the firewall configuration`, `sudo firewall-cmd --add-port=${rule.ports} --permanent`) } else { for (const port of rule.ports) { if (typeof port === 'string') { runCommand(`Adding the ${rule.packageKey} ${rule.ports} rule to the firewall configuration`, `sudo firewall-cmd --add-port=${rule.ports} --permanent`) } else if (port.port && port.proto) { runCommand(`Adding the ${rule.packageKey} ${port.port}/${port.proto} rule to the firewall configuration`, `sudo firewall-cmd --add-port=${port.port}/${port.proto} --permanent`) } else { log('error', logStage, `Unable to parse the firewall definition for ${rule.packageKey}`) } } } } } else { log('error', logStage, `The firewall-cmd executable is not present on the system so the firewall cannot be configured`) } } else if (osType === 'darwin') { const socketFilterFw = '/usr/libexec/ApplicationFirewall/socketfilterfw' const serviceFile = `${process.env.HOME}/.config/firewall/darwin/${rule.packageKey}.sh` if (fileExists(serviceFile)) { runCommand(`Executing the matching ${serviceFile} service file`, `sudo bash "${serviceFile}"`) } else { if (typeof rule.ports === 'string') { log('error', logStage, `_ports rules that are equal to strings are not yet implemented on macOS (package: ${rule.packageKey})`) } else { for (const port of rule.ports) { if (typeof port === 'string') { log('error', logStage, `_ports rules that are equal to strings are not yet implemented on macOS (package: ${rule.packageKey})`) } else if (port.port && port.proto) { if (packageDesc) { runCommand(`Adding new service with description populated for ${rule.packageKey}`, `${socketFilterFw} --add --service "${packageName ? packageName : rule.packageKey}" --setglobaldescription "${packageDesc} --getglobalstate"`) } else { runCommand(`Adding new service for ${rule.packageKey}`, `${socketFilterFw} --add --service "${packageName ? packageName : rule.packageKey}" --getglobalstate`) } runCommand(`Adding firewall rule for ${rule.packageKey}`, `${socketFilterFw} --add --service "${packageName ? packageName : rule.packageKey}" --port ${port.port} --protocol ${port.proto}`) } else { log('error', logStage, `Unable to parse the firewall definition for ${rule.packageKey}`) } } } } } else if (osType === 'windows') { log('warn', logStage, `Windows support not yet added`) } else { log('warn', logStage, `Unknown operating system type`) } } catch (e) { console.log(e) log('error', 'Bin', `Error configuring firewall settings for ${rule.packageKey}`) } } async function updateService(service) { const logStage = 'Service Service' if (osType === 'linux') { const systemctl = which.sync('systemctl', { nothrow: true }) const brew = which.sync('brew', { nothrow: true }) if (systemctl) { try { runCommand(`Starting / enabling ${service} with systemctl`, `sudo systemctl enable --now ${service}`) log('success', logStage, `Started / enabled the ${service} service`) } catch (e) { log('info', logStage, `There was an error starting / enabling the ${service} service with systemd`) try { if (brew) { if (typeof service === 'array') { service.forEach(x => { runCommand(`Starting / enabling object array ${x.name} with Homebrew`, `${x.sudo ? 'sudo brew' : 'brew'} services restart ${x.name}`) log('success', logStage, `Started / enabled the ${x.name} service with Homebrew`) }) } else if (typeof service === 'object') { runCommand(`Starting / enabling object ${service.name} with Homebrew`, `${service.sudo ? 'sudo brew' : 'brew'} services restart ${service.name}`) log('success', logStage, `Started / enabled the ${service.name} service with Homebrew`) } else { runCommand(`Starting / enabling ${service} with Homebrew`, `brew services restart ${service}`) log('success', logStage, `Started / enabled the ${service} service with Homebrew`) } } else { log('error', logStage, `Unable to start service with systemd and Homebrew is not available`) } } catch (err) { log('error', logStage, `Unable to start service with both systemd and Homebrew`) log('info', logStage, `systemd error`) console.error(e) log('info', logStage, `brew services error`) console.error(e) } } } else { log( 'warn', logStage, `The systemctl command is not available so applications with services cannot be started / enabled` ) } } else if (osType === 'darwin') { const brew = which.sync('brew', { nothrow: true }) if (brew) { try { if (typeof service === 'array') { service.forEach(x => { runCommand(`Starting / enabling object array ${x.name} with Homebrew`, `${x.sudo ? 'sudo brew' : 'brew'} services restart ${x.name}`) log('success', logStage, `Started / enabled the ${x.name} service with Homebrew`) }) } else if (typeof service === 'object') { runCommand(`Starting / enabling object ${service.name} with Homebrew`, `${service.sudo ? 'sudo brew' : 'brew'} services restart ${service.name}`) log('success', logStage, `Started / enabled the ${service.name} service with Homebrew`) } else { runCommand(`Starting / enabling ${service} with Homebrew`, `brew services restart ${service}`) log('success', logStage, `Started / enabled the ${service} service with Homebrew`) } } catch (e) { log('error', logStage, `There was an error starting / enabling the ${service} Homebrew service`) console.error(e) } } else { log('warn', logStage, `Homebrew is not available - skipping service start command`) } } } /** * Filter that resolves when all asynchronous filter actions are done */ const asyncFilter = async (arr, predicate) => { const results = await Promise.all(arr.map(predicate)) return arr.filter((_v, index) => results[index]) } async function pruneInstallOrders(installOrders) { const newOrders = Object.assign({}, installOrders) log('info', 'Filter', 'Removing packages from installOrders that are already installed') for (const pkgManager in installOrders) { log('info', 'Filter', `Filtering the ${pkgManager} installOrders`) console.log('List to be filtered:', newOrders[pkgManager]) if (pkgManager === 'appimage') { newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => { try { await runSilentCommand(`zap list | grep "${pkg}" > /dev/null`) return false } catch (e) { return true } }) } else if (pkgManager === 'apt') { newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => { try { await runSilentCommand(`dpkg -l ${pkg} | grep -E '^ii' > /dev/null`) return false } catch (e) { return true } }) } else if (pkgManager === 'brew') { let newVal = newOrders[pkgManager] const pkgTmp = '/tmp/brew-list-install-doctor' runCommand(`Populating temporary file with list of Homebrew packages`, `brew list > ${pkgTmp}`) for (const pkg of newOrders[pkgManager]) { try { await $`cat ${pkgTmp} | grep '^${pkg}$'` log('info', 'Filter', 'Filtering list') newVal = newVal.filter((x) => x !== pkg) console.log('New list', newVal) } catch (e) { // Do nothing } } newOrders[pkgManager] = newVal } else if (pkgManager === 'dnf') { const dnf = which.sync('dnf', { nothrow: true }) newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => { try { if (dnf) { await runSilentCommand(`rpm -qa | grep '$'"${pkg}-" > /dev/null`) } else { await runSilentCommand(`rpm -qa | grep '$'"${pkg}-" > /dev/null`) } return false } catch (e) { return true } }) } else if (pkgManager === 'flatpak') { const flatpakInstallation = await $`flatpak --installations` const flatpakDir = flatpakInstallation.stdout.replace('\n', '') newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => { try { await runSilentCommand(`test -d ${flatpakDir}/app/${pkg}`) return false } catch (e) { return true } }) newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => { try { await runSilentCommand(`flatpak info ${pkg} > /dev/null`) return false } catch (e) { return true } }) } else if (pkgManager === 'pacman') { newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => { try { await runSilentCommand(`pacman -Qs ${pkg} > /dev/null`) return false } catch (e) { return true } }) } else if (pkgManager === 'snap' || pkgManager === 'snap-classic') { newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => { try { await runSilentCommand(`snap list ${pkg} | grep ${pkg} > /dev/null`) return false } catch (e) { return true } }) } log('info', 'Filter', `Finished filtering ${pkgManager}`) console.log('Filtered list:', newOrders[pkgManager]) } return newOrders } async function installPlugins(pluginData) { if (pluginData.cmd && pluginData.plugins && pluginData.plugins.length) { const pluginWhen = pluginData.when console.log('Plugin when condition:', pluginWhen) try { if (pluginWhen) { runCommand(`Checking when condition for ${pluginData.package} plugin - ${plugin}`, pluginWhen) } for (const plugin of pluginData.plugins) { try { const pluginCmd = pluginData.cmd.replace(/{PLUGIN}/g, plugin) try { try { runCommand(`Installing ${pluginData.package} plugin - ${plugin}`, pluginCmd) log('success', 'Plugin', `Successfully installed ${pluginData.package} plugin - ${plugin}`) } catch (e) { log('info', 'Plugin', `Failed to install ${pluginData.package} plugin - ${plugin}`) console.error(e) } } catch (e) { log('info', 'Plugin', `Skipping ${pluginData.package} plugin installs due to failed when condition - ${pluginWhen}`) break } } catch (e) { log('error', 'Plugin', `Failed to install ${pluginData.package} plugin due to an unknown reason - ${plugin}`) console.error(e) } } } catch (e) { log('info', 'Plugin', 'Failed to pass when condition') } } if (pluginData.update) { const pluginDataPackage = pluginData.package if (pluginDataPackage) { runCommand('Updating ' + pluginDataPackage + ' plugins', pluginData.update) } } } async function linkBin(installOrdersBinLink) { let flatpakInstallation, flatpakDir const softwarePackages = installData.softwarePackages const flatpak = which.sync('flatpak', { nothrow: true }) if (flatpak) { flatpakInstallation = await $`flatpak --installations` flatpakDir = flatpakInstallation.stdout.replace('\n', '') } for (const binLink of installOrdersBinLink) { const pkg = softwarePackages[binLink.package][binLink.preference] if (typeof pkg === 'string') { if (binLink.bin !== false) { if (!which.sync(binLink.bin, { nothrow: true })) { if (binLink.preference === 'flatpak' && flatpak) { try { runCommand( `Adding bin link for ${pkg} (${binLink.bin})`, `bash -c 'test -d ${flatpakDir}/app/${pkg} && mkdir -p "${process.env.HOME}/.local/bin/flatpak" && echo "flatpak run ${pkg} \\\$*" > "${process.env.HOME}/.local/bin/flatpak/${binLink.bin}" && chmod +x "${process.env.HOME}/.local/bin/flatpak/${binLink.bin}"'` ) log('success', 'Bin', `Linked ~/.local/bin/flatpak/${binLink.bin} to the ${pkg} Flatpak`) } catch (e) { log('warn', 'Bin', `Expected flatpak directory not available - ${flatpakDir}/app/${pkg}`) } } else if (softwarePackages[binLink.package]['_app']) { try { const appName = softwarePackages[binLink.package]['_app'] log('info', 'Bin', `Checking for existence of ${appName} application in /Applications and ~/Applications`) if (fileExists(`/Applications/${appName}`)) { runCommand( `Adding shortcut bin link for ${binLink.package}`, `bash -c 'mkdir -p "${process.env.HOME}/.local/bin/cask" && echo "open \"/Applications/${appName}\" \\\$*" > "${process.env.HOME}/.local/bin/cask/${binLink.bin}" && chmod +x "${process.env.HOME}/.local/bin/cask/${binLink.bin}"'` ) } else if(fileExists(`${process.env.HOME}/Applications/${appName}`)) { runCommand( `Adding shortcut bin link for ${binLink.package}`, `bash -c 'mkdir -p "${process.env.HOME}/.local/bin/cask" && echo "open \"${process.env.HOME}/Applications/${appName}\" \\\$*" > "${process.env.HOME}/.local/bin/cask/${binLink.bin}" && chmod +x "${process.env.HOME}/.local/bin/cask/${binLink.bin}"'` ) } else { log('warn', 'Bin', `Expected Homebrew cask directory not found - ${pkg}`) } } catch (e) { console.log(e) log('warn', 'Bin', `Error creating bin shortcut link for ${pkg}`) } } } else { log('info', 'Bin', `Link already exists for ${binLink.package}`) } } else { log('info', 'Bin', `Skipping ${binLink.package} because the _bin is equal to false`) } } else { log('info', 'Bin', `Skipping ${binLink.package} because there was more than one _bin value`) } } } // main process async function installSoftware(pkgsToInstall) { osType = await OSType() osID = osType if (osType === 'linux') { osID = await releaseID() } log('info', 'Catalog Download', `Fetching the latest version of the installation map`) installData = await downloadInstallData() chezmoiData = await getChezmoiData() log('info', 'Filter', `Calculating the install orders`) await generateInstallOrders(pkgsToInstall ? pkgsToInstall : process.argv.slice(3)) const packageManagers = Object.keys(installOrders) packageManagers.length && log('info', 'Pre-Reqs', `Ensuring any package managers that will be used are installed / configured`) for (const packageManager of packageManagers) { await ensurePackageManager(packageManager) } try { for (const key in installOrders) { installOrders[key] = [...new Set(installOrders[key])] } log('info', 'Install', `The install orders were generated:`) } catch (e) { log('error', 'Filter', `There was an error reducing the duplicates in the install orders`) console.error(e) } installOrders = await pruneInstallOrders(installOrders) delete installOrders._deps console.log('Install orders:', installOrders) packageManagers.length && log('info', 'Pre-Reqs', `Running package manager pre-installation steps`) for (const packageManager of packageManagers) { await beforeInstall(packageManager) } installOrdersPre.length && log('info', 'Pre-Install', `Running package-specific pre-installation steps`) for (const script of installOrdersPre) { const timeoutAvail = which.sync('timeout', { nothrow: true }) if (timeoutAvail) { await $`${['timeout', '1000', 'bash', '-c', script]}` } else { await $`${['bash', '-c', script]}` } } installOrdersGroups.length && log('info', 'Users / Groups', `Adding groups / users`) for (const group of installOrdersGroups) { await addUserGroup(group) } packageManagers.length && log('info', 'Install', `Installing the packages`) for (const packageManager of packageManagers) { const asyncOrders = [] asyncOrders.push(installPackageList(packageManager, installOrders[packageManager])) await Promise.all(asyncOrders) } installOrdersPorts.length && log('info', 'Firewall', 'Configuring firewall exceptions') for (const firewallRule of installOrdersPorts) { await addFirewallRule(firewallRule) } installOrdersService.length && log('info', 'Post-Install', `Running package-specific post-installation steps`) for (const service of installOrdersService) { await updateService(service) } if (!binLinkRan) { binLinkRan = true log('info', 'Bin', 'Linking bin aliases to their installed packages') await linkBin(installOrdersBinLink) } for (const script of installOrdersPost) { try { log('info', 'Post Hook', script) const timeoutAvail = which.sync('timeout', { nothrow: true }) if (timeoutAvail) { await $`${['timeout', '1000', 'bash', '-c', script]}` } else { await $`${['bash', '-c', script]}` } } catch(e) { log('info', 'Post-Install Hook', 'Encountered error while running post-install hook') } } installOrdersPlugins.length && log('info', 'Plugin', 'Installing package-specific plugins') for (const plugin of installOrdersPlugins) { await installPlugins(plugin) } packageManagers.length && log('info', 'Post-Install', `Running package manager post-installation steps`) for (const packageManager of packageManagers) { await afterInstall(packageManager) } log('success', 'Complete', `Done!`) } // Start the main process await installSoftware(false)