install.fairie/home/dot_local/bin/executable_install-program
2023-01-04 02:05:12 +00:00

1330 lines
50 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
if (type === 'info') {
icon = chalk.cyanBright(figures.pointer)
message = chalk.gray.bold(msg)
} else if (type === 'star') {
icon = chalk.yellowBright(figures.star)
message = chalk.bold(msg)
} else if (type === 'success') {
icon = chalk.greenBright(figures.play)
message = chalk.bold(msg)
} else if (type === 'warn') {
icon = `${chalk.yellowBright(figures.lozenge)} ${chalk.bold.black.bgYellowBright(' WARNING ')}`
message = chalk.yellowBright(msg)
} else if (type === 'error') {
icon = `${chalk.redBright(figures.cross)} ${chalk.black.bold.bgRedBright(' ERROR ')}`
message = chalk.redBright(msg)
}
const outputMessage = `${icon} ${chalk.bold(label)} ${message}`
console.log(outputMessage)
}
let installData
const installOrders = {}
const installOrdersPre = []
const installOrdersPost = []
const installOrdersSystemd = []
let brewUpdated, osType, osID, snapRefreshed
// Download the installation map
async function downloadInstallData() {
const response = await fetch('https://gitlab.com/megabyte-labs/hiawatha-dotfiles/-/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(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`)
continue
}
for (let preference of preferenceOrder) {
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', 'Skipping Package', `${pkg} is being skipped because of the _when:${pref} condition`)
continue pkgFor
}
} else if (scopedPkgManager) {
try {
await $scopedPkgManager
} catch (e) {
const pref = preference
log('info', 'Skipping Package', `${pkg} is being skipped because of the _when:${pref} condition`)
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', 'Skipping Package', `${pkg} is being skipped because of the _when:${pref} condition`)
continue pkgFor
}
} else if (normalCheck) {
try {
await $(normalCheck)
} catch (e) {
log('info', 'Skipping Package', `${pkg} is being skipped because of the _when condition`)
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', 'Skipping Package', `${pkg} is being skipped because of the _bin:${pref} condition`)
log('info', 'Skipping Package', `${bin} is already in the PATH`)
continue pkgFor
}
} 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', 'Skipping Package', `${pkg} is being skipped because of the _bin:${pref} condition`)
log('info', 'Skipping Package', `${bin} is already in the PATH`)
continue pkgFor
}
} 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', 'Skipping Package', `${pkg} is being skipped because of the _bin:${pref} condition`)
log('info', 'Skipping Package', `${bin} is already in the PATH`)
continue pkgFor
}
} 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', 'Skipping Package', `${pkg} is being skipped because of the _bin condition`)
log('info', 'Skipping Package', `${bin} is already in the PATH`)
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]
if (doubleScoped) {
let pref
if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID]) {
pref = preference + ':' + osID
log('info', 'Installing Dependencies', `Installing dependencies for ${packageKey}.${pref}`)
await generateInstallOrders(softwarePackages[packageKey][pref])
} else if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType]) {
pref = preference + ':' + osType
log('info', 'Installing Dependencies', `Installing dependencies for ${packageKey}.${pref}`)
await generateInstallOrders(softwarePackages[packageKey][pref])
} else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference]) {
pref = osID + ':' + preference
log('info', 'Installing Dependencies', `Installing dependencies for ${packageKey}.${pref}`)
await generateInstallOrders(softwarePackages[packageKey][pref])
} else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference]) {
pref = osType + ':' + preference
log('info', 'Installing Dependencies', `Installing dependencies for ${packageKey}.${pref}`)
await generateInstallOrders(softwarePackages[packageKey][pref])
}
} else if (scopedPkgManager) {
const pref = preference
log('info', 'Installing Dependencies', `Installing dependencies for ${packageKey}.${pref}`)
await generateInstallOrders(softwarePackages[packageKey][pref])
} else if (scopedSystem) {
let pref
if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID]) {
pref = osID
log('info', 'Installing Dependencies', `Installing dependencies for ${packageKey}.${pref}`)
await generateInstallOrders(softwarePackages[packageKey][pref])
} else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType]) {
pref = osType
log('info', 'Installing Dependencies', `Installing dependencies for ${packageKey}.${pref}`)
await generateInstallOrders(softwarePackages[packageKey][pref])
}
} else if (normalCheck) {
log('info', 'Installing Dependencies', `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
}
}
}
}
if (generateInstallOrderCount === 1) {
return installOrders
} else {
generateInstallOrderCount--
}
}
// 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.concat(typeof preHook === 'string' ? [preHook] : preHook)
}
const postHook = getHook(packages, 'post', scopedPreference, preference)
if (postHook) {
installOrdersPost.concat(typeof postHook === 'string' ? [postHook] : postHook)
}
const systemdHook = getHook(packages, 'systemd', scopedPreference, preference)
if (systemdHook) {
installOrdersSystemd.concat(typeof systemdHook === 'string' ? [systemdHook] : systemdHook)
}
if (!installOrders[preference]) {
installOrders[preference] = []
}
log('info', 'Install Orders', `Found a match for the package \`${pkg}\` (${packageKey} via ${scopedPreference})`)
const newPackages = packages[scopedPreference]
const newPkgs = typeof newPackages === 'string' ? [newPackages] : newPackages
if (preference === 'snap' && softwarePackages[pkg]['_snapClassic'] === true) {
if (!installOrders[preference + '-classic']) {
installOrders[preference + '-classic'] = []
}
installOrders[preference + '-classic'] = installOrders[preference].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 '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 {
await $`sudo apt-get autoclean`
await $`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') {
} 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 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')
}
await $`sudo apt-get install -y ${dep}`
} else if (dnf) {
if (updateDone['dnf'] !== true) {
await beforeInstall('dnf')
}
await $`sudo dnf install -y ${dep}`
} else if (yum) {
if (updateDone['yum'] !== true) {
await beforeInstall('yum')
}
await $`sudo yum install -y ${dep}`
} else if (pacman) {
if (updateDone['pacman'] !== true) {
await beforeInstall('pacman')
}
await $`sudo pacman -Sy ${dep}`
} else if (zypper) {
if (updateDone['zypper'] !== true) {
await beforeInstall('zypper')
}
await $`sudo zypper 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') {
} 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'
}
await $`${unbufferPrefix} ansible 127.0.0.1 -vv -e '{ ansible_connection: "local", ansible_user: "${process.env.USER}", install_homebrew: False }' -m setup`
} else if (packageManager === 'apk') {
await $`sudo apk update`
} else if (packageManager === 'apt') {
await $`sudo apt-get update`
} else if (packageManager === 'basher') {
} else if (packageManager === 'binary') {
} else if (packageManager === 'brew' || packageManager === 'cask') {
if (!brewUpdated) {
brewUpdated = true
await $`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) {
await $`sudo dnf update -y`
} else if (yum) {
await $`sudo yum update -y`
}
} else if (packageManager === 'flatpak') {
await $`sudo flatpak update`
} else if (packageManager === 'gem') {
} else if (packageManager === 'go') {
} else if (packageManager === 'nix') {
await $`nix-channel --update`
} else if (packageManager === 'npm') {
} else if (packageManager === 'pacman') {
await $`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) {
await $`sudo port sync`
} else {
log('error', 'Port Not Installed', 'Skipping sudo port sync step because port is not installed')
}
} else if (packageManager === 'scoop') {
await $`scoop update`
} else if (packageManager === 'snap' || packageManager === 'snap-classic') {
if (!snapRefreshed) {
snapRefreshed = true
await $`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 $`docker run --rm hello-world`
} 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') {
await $`winget source update`
} else if (packageManager === 'yay') {
} else if (packageManager === 'zypper') {
await $`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 = 'Package Manager Install'
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 https://github.com/srevinsaju/zap/releases/download/continuous/zap-amd64 > /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) {
await $`sudo apk add flatpak`
} else if (apt) {
await $`
sudo apt-get install -y flatpak
if [ -f /usr/bin/gnome-shell ]; then
sudo apt-get install -y gnome-software-plugin-flatpak
fi
if [ -f /usr/bin/plasmashell ]; then
sudo apt-get install -y plasmashell
fi
`
} 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`
}
const flatpakPost = which.sync('flatpak', { nothrow: true })
if (flatpakPost) {
await $`flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo`
} else {
log('error', logStage, `\`flatpak\` failed to install!`)
}
log('info', logStage, `\`flatpak\` was installed. It may require a reboot to function correctly.`)
}
} else if (packageManager === 'gem') {
await ensureInstalled('gem', $`brew install ruby`)
} else if (packageManager === 'go') {
await ensureInstalled('gem', $`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) {
await $`if [ -f /etc/apt/preferences.d/nosnap.pref ]; then sudo mv /etc/apt/preferences.d/nosnap.pref /etc/apt/nosnap.pref.bak; fi && 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
} else if (dnf) {
await $`sudo dnf install -y snapd && if [ ! -d /snap ]; then sudo ln -s /var/lib/snapd/snap /snap; fi`
} else if (yum) {
await $`sudo yum install -y snapd && sudo systemctl enable --now snapd.socket && if [ ! -d /snap ]; then sudo ln -s /var/lib/snapd/snap /snap; fi`
} else if (pacman) {
await $`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) {
await $`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, 3) === 'http' && pkg.slice(-8) === 'AppImage') {
log('info', 'AppImage Install', `Installing ${pkg} from its URL`)
await $`zap install --from ${pkg}`
} else if (pkg.includes('/')) {
log('info', 'AppImage Install', `Installing ${pkg} from a GitHub Release`)
await $`zap install --github --from ${pkg}`
} else {
log('info', 'AppImage Install', `Installing ${pkg} using the AppImage Catalog`)
await $`zap install ${pkg}`
}
} catch (e) {
log('error', 'AppImage / Zap Failure', `There was an error using Zap to install ${pkg}`)
}
}
} 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 === 'on' ? 'vv' : ''
await $`${unbufferPrefix} ansible 127.0.0.1 -v${verboseMode} -e '{ ansible_connection: "local", ansible_user: "${process.env.USER}", install_homebrew: False }' -m include_role -a name=${pkg}`
} catch (e) {
log('error', 'Ansible Role Failure', `There was an error installing ${pkg} with Ansible`)
}
}
} else if (packageManager === 'apk') {
for (let pkg of packages) {
try {
await $`sudo apk add ${pkg}`
} catch (e) {
log('error', 'APK Install Failure', `There was an error installing ${pkg} with apk`)
}
}
} else if (packageManager === 'apt') {
for (let pkg of packages) {
try {
await $`sudo apt-get install -y ${pkg}`
} catch (e) {
log('error', 'apt-get Failure', `There was an error installing ${pkg} with apt-get`)
}
}
} else if (packageManager === 'basher') {
for (let pkg of packages) {
try {
await $`basher install ${pkg}`
} catch (e) {
log('error', 'Basher Failure', `There was an error installing ${pkg} with basher`)
}
}
} else if (packageManager === 'binary') {
for (let pkg of packages) {
try {
await $`TMP="$(mktemp)" && curl -sSL ${pkg} > "$TMP" && sudo mv "$TMP" /usr/local/src/${binName} && chmod +x /usr/local/src/${binName}`
} catch (e) {
log('error', 'Binary Release Install', `There was an error installing the binary release for ${pkg}`)
}
}
} else if (packageManager === 'brew') {
for (let pkg of packages) {
try {
await $`brew install ${pkg}`
} catch (e) {
log('error', 'Homebrew Failure', `There was an error installing ${pkg} with brew`)
}
}
} else if (packageManager === 'cask') {
for (let pkg of packages) {
try {
await $`brew install --cask ${pkg}`
} catch (e) {
log('error', 'Homebrew Cask Failure', `There was an error installing ${pkg} with Homebrew Cask`)
}
}
} else if (packageManager === 'cargo') {
for (const pkg of packages) {
try {
await $`cargo install ${pkg}`
} catch (e) {
log('error', 'Cargo Failure', `There was an error installing ${pkg} with Cargo`)
}
}
} else if (packageManager === 'choco') {
for (let pkg of packages) {
try {
await $`choco install -y ${pkg}`
} catch (e) {
log('error', 'Chocolatey Failure', `There was an error installing ${pkg} with Chocolatey`)
}
}
} else if (packageManager === 'crew') {
} else if (packageManager === 'dnf') {
const dnf = which.sync('dnf', { nothrow: true })
const yum = which.sync('yum', { nothrow: true })
if (dnf) {
for (let pkg of packages) {
try {
await $`sudo dnf install -y ${pkg}`
} catch (e) {
log('error', 'dnf Failure', `There was an error installing ${pkg} with dnf`)
}
}
} else if (yum) {
for (let pkg of packages) {
try {
await $`sudo yum install -y ${pkg}`
} catch (e) {
log('error', 'yum Failure', `There was an error installing ${pkg} with yum`)
}
}
}
} else if (packageManager === 'flatpak') {
for (let pkg of packages) {
try {
await $`sudo flatpak install -y flathub ${pkg}`
} catch (e) {
log('error', 'Flatpak Failure', `There was an error installing ${pkg} with flatpak`)
}
}
} else if (packageManager === 'gem') {
for (let pkg of packages) {
try {
await $`gem install ${pkg}`
} catch (e) {
log('error', 'Gem Failure', `There was an error installing ${pkg} with gem`)
}
}
} else if (packageManager === 'go') {
for (let pkg of packages) {
try {
await $`go install ${pkg}`
} catch (e) {
log('error', 'Go Failure', `There was an error installing ${pkg} with go`)
}
}
} else if (packageManager === 'nix') {
} else if (packageManager === 'npm') {
for (let pkg of packages) {
try {
await $`volta install ${pkg}`
} catch (e) {
log('error', 'Volta Failure', `There was an error installing ${pkg} with volta`)
}
}
} else if (packageManager === 'pacman') {
for (let pkg of packages) {
try {
await $`sudo pacman -Sy --noconfirm --needed ${pkg}`
} catch (e) {
log('error', 'Pacman Failure', `There was an error installing ${pkg} with pacman`)
}
}
} else if (packageManager === 'pipx') {
for (let pkg of packages) {
try {
await $`pipx install ${pkg}`
} catch (e) {
log('error', 'PIPX Failure', `There was an error installing ${pkg} with pipx`)
}
}
} else if (packageManager === 'pkg') {
} else if (packageManager === 'port') {
const port = which.sync('port', { nothrow: true })
if (port) {
for (let pkg of packages) {
try {
await $`sudo port install ${pkg}`
} catch (e) {
log('error', 'Port Failure', `There was an error installing ${pkg} with port`)
}
}
} 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 {
await $`scoop install ${pkg}`
} catch (e) {
log('error', 'Scoop Failure', `There was an error installing ${pkg} with scoop`)
}
}
} else if (packageManager === 'snap') {
for (let pkg of packages) {
try {
await $`sudo snap install ${pkg}`
} catch (e) {
log('error', 'Snap Failure', `There was an error installing ${pkg} with snap`)
}
}
} else if (packageManager === 'script') {
for (let pkg of packages) {
try {
await $`${pkg}`
} catch (e) {
log('error', 'Script Install Failure', `There was an error running the script installation method for ${pkg}`)
}
}
} else if (packageManager === 'snap-classic') {
for (let pkg of packages) {
try {
await $`sudo snap install --classic ${pkg}`
} catch (e) {
log('error', 'Snap Failure', `There was an error installing ${pkg} with snap in classic mode`)
}
}
} else if (packageManager === 'whalebrew') {
for (let pkg of packages) {
try {
await $`whalebrew install ${pkg}`
} catch (e) {
log('error', 'Whalebrew Failure', `There was an error installing ${pkg} with whalebrew`)
}
}
} else if (packageManager === 'winget') {
for (let pkg of packages) {
try {
await $`winget install ${pkg}`
} catch (e) {
log('error', 'Winget Failure', `There was an error installing ${pkg} with winget`)
}
}
} else if (packageManager === 'yay') {
for (let pkg of packages) {
try {
await $`yay -Sy --noconfirm --needed ${pkg}`
} catch (e) {
log('error', 'Yay Failure', `There was an error installing ${pkg} with yay`)
}
}
} else if (packageManager === 'zypper') {
for (let pkg of packages) {
try {
await $`sudo zypper install -y ${packages}`
} catch (e) {
log('error', 'Zypper Failure', `There was an error installing ${pkg} with zypper`)
}
}
}
} catch (e) {
log('error', logStage, `Possibly encountered an error while installing via \`${packageManager}\``)
log('info', logStage, `Proceeding with the installation..`)
}
}
async function updateSystemd(service) {
const logStage = 'Systemd Service'
if (osType === 'linux') {
const systemd = which.sync('systemctl', { nothrow: true })
if (systemd) {
try {
log('info', logStage, `Starting / enabling the ${service} service`)
await $`sudo systemctl enable --now ${service}`
} catch (e) {
log('error', logStage, `There was an error starting / enabling the ${service} service`)
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 {
log('info', logStage, `Starting / enabling the ${service} service with Homebrew`)
await $`brew services start ${service}`
} 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`)
}
}
}
// 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', 'Install Orders', `Calculating the install orders`)
await generateInstallOrders(pkgsToInstall ? pkgsToInstall : process.argv.slice(3))
log(
'info',
'Ensure Package Manager Installed',
`Ensuring any package managers that will be used are installed / configured`
)
const packageManagers = Object.keys(installOrders)
for (const packageManager of packageManagers) {
await ensurePackageManager(packageManager)
}
try {
for (const key in installOrders) {
installOrders[key] = [...new Set(installOrders[key])]
}
log('info', 'Install Orders', `The install orders were generated:`)
} catch (e) {
log('error', 'Install Orders', `There was an error reducing the duplicates in the install orders`)
console.error(e)
}
console.log(installOrders)
log('info', 'Package Manager Pre-Install', `Running package manager pre-installation steps`)
for (const packageManager of packageManagers) {
await beforeInstall(packageManager)
}
log('info', 'Package Pre-Install', `Running package-specific pre-installation steps`)
for (const script of installOrdersPre) {
await $`${script}`
}
log('info', 'Package Install', `Installing the packages`)
for (const packageManager of packageManagers) {
const asyncOrders = []
asyncOrders.push(installPackageList(packageManager, installOrders[packageManager]))
await Promise.all(asyncOrders)
}
log('info', 'Package Post-Install', `Running package-specific post-installation steps`)
for (const service of installOrdersSystemd) {
await updateSystemd(service)
}
for (const script of installOrdersPost) {
await $`${script}`
}
log('info', 'Package Manager Post-Install', `Running package manager post-installation steps`)
for (const packageManager of packageManagers) {
await afterInstall(packageManager)
}
log('success', 'Installation Complete', `Done!`)
}
// Start the main process
await installSoftware(false)