install.fairie/home/dot_local/bin/executable_install-program
Brian Zalewski 285d7ba92f Update 3 files
- /home/dot_config/task/Taskfile.yml
- /home/dot_local/bin/executable_install-program
- /software.yml
2023-04-15 23:34:26 +00:00

1804 lines
71 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 outputMessage = `${icon} ${message}`
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}`, {
stdio: 'inherit',
shell: true,
// Timeout of 10m
timeout: 1000 * 60 * 10
})
}
async function runSilentCommand(command) {
execSync(`${command}`, { stdio: 'inherit', shell: true })
}
function fileExists(pathToFile) {
return fs.existsSync(pathToFile)
}
let installData
let installOrders = {}
let installMeta = {}
let binLinkRan = false
let installOrdersPre = []
let installOrdersPost = []
let installOrdersService = []
let installOrdersGroups = []
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://gitlab.com/megabyte-labs/install.doctor/-/raw/master/software.yml')
if (response.ok) {
const text = await response.text()
return YAML.parse(text)
} 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()
return YAML.parse(text)
}
}
// 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`)
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 $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
}
log('info', 'Filter', `${pkg} is being skipped because of the _when:${pref} condition`)
processPluginOrders(pkg)
continue pkgFor
}
} else if (scopedPkgManager) {
try {
await $scopedPkgManager
} catch (e) {
const pref = preference
log('info', 'Filter', `${pkg} is being skipped because of the _when:${pref} condition`)
processPluginOrders(pkg)
continue pkgFor
}
} else if (scopedSystem) {
try {
await $scopedSystem
} catch (e) {
let pref
if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID]) {
pref = osID
} else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType]) {
pref = osType
}
log('info', 'Filter', `${pkg} is being skipped because of the _when:${pref} condition`)
processPluginOrders(pkg)
continue pkgFor
}
} else if (normalCheck) {
try {
await $(normalCheck)
} catch (e) {
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
}
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
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
}
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) {
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 _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 = installData && installData.softwarePlugins && installData.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)
if (serviceHook) {
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)
}
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) {
try {
await $`test -d /etc/ubuntu-advantage`
return 'ubuntu'
} catch (e) {
return 'apt'
}
} else if (dnf || yum) {
return 'dnf'
} else if (pacman) {
return 'pacman'
} else if (zypper) {
return 'zypper'
} else if (freebsdPkg && freebsdVersion) {
return 'freebsd'
} else {
try {
await $`test -d /Applications && test -d /Library`
return 'darwin'
} catch (e) {
return '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() {
try {
await $`test -d /Applications && test -d /Library`
return 'darwin'
} catch (e) {
try {
await $`test -f /etc/os-release`
return 'linux'
} catch (e) {
return '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 ANSIBLE INSTALL/d' /etc/sudoers`
} else {
await $`sudo sed -i '/# TEMPORARY FOR ANSIBLE INSTALL/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 ${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" | 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
runCommand('Running brew update', `brew update`)
}
} 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 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`, $`open /Applications/Docker.app`]
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)"
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="$?"
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') {
await $`brew install rustup`
await $`rustup-init`
} 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 ruby`)
}
} else if (packageManager === 'go') {
const go = which.sync('go', { nothrow: true })
if (!go) {
await ensureInstalled('go', $`brew install 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 volta`
}
await $`
if [ -z "$VOLTA_HOME" ]; then
volta setup
fi
export PATH="$VOLTA_HOME/bin:$PATH"
volta install node
`
}
log('info', logStage, 'Ensuring Volt has Node.js runtime available')
await $`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 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`)
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 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 {
runCommand(
`Installing ${pkg} via ${packageManager}`,
`sudo 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 ${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 ${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('error', 'Install', `There was an error installing ${pkg} with pipx`)
console.error(e)
}
}
} else if (packageManager === 'pkg') {
} 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`)
if (osType === 'linux') {
const useradd = which.sync('useradd', { nothrow: true })
if (useradd) {
runCommand(`Adding the ${group} user / group`, `sudo useradd ${group}`)
} else {
log('error', logStage, `The useradd command is unavailable`)
}
} else if (osType === 'darwin') {
runCommand(`Adding the ${group} group`, `sudo dscl . -create /Users/${group}`)
runCommand(`Creating the ${group} group`, `sudo dscl . -create /Groups/${group}`)
runCommand(`Adding the ${group} user to the ${group} group`, `sudo dscl . -append /Groups/${group} GroupMembership ${group}`)
} else if (osType === 'windows') {
log('warn', logStage, `Windows support not yet added`)
} else {
log('warn', logStage, `Unknown operating system type`)
}
}
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) {
runCommand(`Starting / enabling ${service} with Homebrew`, `brew services start ${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 {
runCommand(`Starting / enabling ${service} with Homebrew`, `brew services start ${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}$'`
newVal = newVal.filter((x) => x === pkg)
} 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) {
for (const plugin of pluginData.plugins) {
try {
const pluginCmd = pluginData.cmd.replace(/{PLUGIN}/g, plugin)
runCommand(`Installing ${pluginData.package} plugin - ${plugin}`, pluginCmd)
log('success', 'Plugin', `Successfully installed ${pluginData.package} plugin - ${plugin}`)
} catch (e) {
log('error', 'Plugin', `Failed to install ${pluginData.package} plugin - ${plugin}`)
console.error(e)
}
}
}
}
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 (!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 (binLink.preference === 'cask') {
try {
const caskWhen = softwarePackages[binLink.package]['_when:cask']
const caskDir = caskWhen.replace('! test -d ', '')
if (fileExists(caskDir)) {
runCommand(
`Adding shortcut bin link for ${binLink.package}`,
`bash -c 'mkdir -p "${process.env.HOME}/.local/bin/cask" && echo "open ${caskDir} \\\$*" > "${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) {
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 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()
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)
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) {
await $`${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)
}
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 {
await $`${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)