#!/usr/bin/env zx const execSync = require('child_process').execSync $.log = (entry) => { if (entry.kind === 'cmd' && entry.cmd.substring(0, 4) === 'logg') { //execSync(entry.cmd, {stdio: 'inherit', shell: true}) } else { log(entry) } } let installData const installOrders = {}; const installOrdersPre = []; const installOrdersPost = []; const osType = await OSType(); let osID = osType if(osType === 'linux') { osID = await realeaseID() } // Download the installation map async function downloadInstallData() { const response = await fetch( "https://gitlab.com/megabyte-labs/misc/dotfiles/-/raw/master/.local/share/chezmoi/software.yml" ); if (response.ok) { const text = await response.text() return YAML.parse(text) } else { await $`logg error 'Failed to download the installation map'` } } // Creates the installOrders object which maps package managers to arrays of packages to install async function generateInstallOrders() { const packagesToInstall = process.argv.slice(3); const installerPreference = await OSTypeInstallerKey() await $`logg info 'Installer preference category detected as ${installerPreference}'` const preferenceOrder = installData.installerPreference[installerPreference]; await $`logg info 'Preference order acquired:'` console.log(preferenceOrder) const softwarePackages = installData.softwarePackages; 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 { await $`logg warn 'The package \`${pkg}\` was not found in the installation map'` continue; } for (let preference of preferenceOrder) { if (softwarePackages[packageKey][preference + ":" + osID]) { await updateInstallMaps( preference, softwarePackages[packageKey], preference + ":" + osID, pkg, packageKey ); break } else if (softwarePackages[packageKey][preference + ":" + osType]) { await updateInstallMaps( preference, softwarePackages[packageKey], preference + ":" + osType, pkg, packageKey ); break } else if (softwarePackages[packageKey][preference]) { await updateInstallMaps(preference, softwarePackages[packageKey], preference, pkg, packageKey); break } } } return installOrders; } // Update install, pre-hook, and post-hook objects async function updateInstallMaps(preference, packages, scopedPreference, pkg, packageKey) { 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 ); } if (!installOrders[preference]) { installOrders[preference] = []; } await $`logg info 'Found a match for the package \`${pkg}\` (${packageKey} via ${scopedPreference})'` const newPackages = packages[scopedPreference]; installOrders[preference] = installOrders[preference].concat( typeof newPackages === "string" ? [newPackages] : newPackages ); } // 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() { const apt = which.sync('apt-get', { nothrow: true }) const dnf = which.sync('dnf', { nothrow: true }) const freebsd = which.sync('pkg', { 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 (freebsd) { return 'freebsd' } else { try { await $`test -d /Applications && test -d /Library` return 'darwin' } catch (e) { return 'windows' } } } // 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) { if (packageManager === 'appimage') { } else if (packageManager === 'ansible') { await $`logg info 'Ensuring temporary passwordless sudo privileges used by Ansible are removed'` await $`sudo sed -i '/# TEMPORARY FOR ANSIBLE INSTALL/d' /etc/sudoers` } else if (packageManager === 'apk') { } else if (packageManager === 'apt') { } 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 === 'crew') { } else if (packageManager === 'dnf') { } else if (packageManager === 'flatpak') { } else if (packageManager === 'snap') { } else if (packageManager === 'whalebrew') { } else if (packageManager === 'winget') { } else if (packageManager === 'yay') { } else if (packageManager === 'zypper') { } } // Pre-install hook async function beforeInstall(packageManager) { if (packageManager === 'appimage') { } else if (packageManager === 'ansible') { await $`logg info 'Temporarily enabling passwordless sudo for Ansible role installations'` await $`sudo echo "$(whoami) ALL=(ALL:ALL) NOPASSWD: ALL # TEMPORARY FOR ANSIBLE INSTALL" > /etc/sudoers` } else if (packageManager === 'apk') { } else if (packageManager === 'apt') { await $`sudo apt-get update` } 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') { await $`sudo pacman -Syu` } else if (packageManager === 'pipx') { } else if (packageManager === 'pkg') { } else if (packageManager === 'port') { } else if (packageManager === 'scoop') { } else if (packageManager === 'crew') { } else if (packageManager === 'dnf') { } else if (packageManager === 'flatpak') { } else if (packageManager === 'snap') { } 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) { await $`logg warn 'The command \`docker run --rm hello-world\` failed'` try { const promises = [$`test -d /Applications/Docker.app`, $`logg info 'Attempting to open \`/Applications/Docker.app\` (Docker Desktop for macOS). This should take about 30 seconds.'`, $`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) { await $`logg warn 'Docker Desktop appears to not be installed!'` } } } } else if (packageManager === 'winget') { } else if (packageManager === 'yay') { } else if (packageManager === 'zypper') { } } async function ensureInstalled(bin, callback) { const installed = which.sync(bin, { nothrow: true }) if (installed) { await $`logg info '\`${bin}\` is available'` } else { await $`logg warn '\`${bin}\` is not installed!'` if (callback) { await callback } else { await $`logg error 'There does not appear to be an installation method available for \`${bin}\`'` } } } // Ensure the package manager is available let packageManagerInstalled = {}; async function ensurePackageManager(packageManager) { await $`logg info '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') { } else if (packageManager === 'ansible') { $` if [ ! -f "$HOME/.cache/megabytelabs/ansible-installed" ] || ! command -v ansible > /dev/null; then pipx install ansible pipx inject ansible PyObjC PyObjC-core docker lxml netaddr pexpect python-vagrant pywinrm requests-credssp watchdog touch "$HOME/.cache/megabytelabs/ansible-installed" else logg info '\`ansible\` and its supporting packages appear to be installed' fi ` } 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') { } 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 logg info '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 logg warn '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') { await ensureInstalled('cargo', $` # TODO Bash script that installs 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) { await $`logg info '\`dnf\` is available'` } else if (yum) { await $`logg info '\`yum\` is available'` } else { await $`logg error 'Both \`dnf\` and \`yum\` are not available'` } } else if (packageManager === 'flatpak') { const flatpak = which.sync('flatpak', { nothrow: true }) if (flatpak) { await $`logg info '\`flatpak\` is available'` } else { const apk = which.sync('apk', { nothrow: true }) const apt = which.sync('apt', { 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) { $`sudo apk add flatpak` } else if(apt) { $` sudo apt install -y flatpak if [ -f /usr/bin/gnome-shell ]; then sudo apt install -y gnome-software-plugin-flatpak fi if [ -f /usr/bin/plasmashell ]; then sudo apt 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 { await $`logg error '\`flatpak\` failed to install!'` } await $`logg info '\`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('npm', { nothrow: true }) const node = which('node', { nothrow: true }) const volta = which('volta', { nothrow: true }) if (npm && node && volta) { await $`logg info '\`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 ` } } else if (packageManager === 'pacman') { await ensureInstalled('pacman', false) } else if (packageManager === 'pipx') { await ensureInstalled('pipx', $`brew install pipx && pipx ensurepath`) } else if (packageManager === 'pkg') { await ensureInstalled('pkg', false) } else if (packageManager === 'port') { await ensureInstalled('port', $` echo -n "TODO - script that installs port on macOS here" `) } 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 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) { $`sudo snap install core` } } 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) { try { let pkg = packages if (packageManager === 'appimage') { } else if (packageManager === 'ansible') { for (const role of packages) { await $`ansible localhost -m include_role -a name=${role}` } } else if (packageManager === 'apk') { await $`sudo apk add ${packages}` } else if (packageManager === 'apt') { await $`sudo apt-get install -y ${packages}` } else if (packageManager === 'basher') { } else if (packageManager === 'binary') { } else if (packageManager === 'brew') { await $`brew install ${packages}` } else if (packageManager === 'cask') { await $`brew install --cask ${packages}` } else if (packageManager === 'cargo') { for (const pkg of packages) { await $`cargo install ${pkg}` } } else if (packageManager === 'choco') { await $`choco install -y ${packages}` } else if (packageManager === 'crew') { } else if (packageManager === 'dnf') { const dnf = which.sync('dnf', { nothrow: true }) const yum = which.sync('yum', { nothrow: true }) if (dnf) { await $`dnf install -y ${packages}` } else if (yum) { await $`yum install -y ${packages}` } } else if (packageManager === 'flatpak') { for (let pkg of packages) { await $`sudo flatpak install flathub ${pkg}` } } else if (packageManager === 'gem') { for (let pkg of packages) { await $`gem install ${pkg}` } } else if (packageManager === 'go') { for (let pkg of packages) { await $`go install ${pkg}` } } else if (packageManager === 'nix') { } else if (packageManager === 'npm') { for (let pkg of packages) { await $`volta install ${pkg}` } } else if (packageManager === 'pacman') { await $`sudo pacman -Sy --noconfirm --needed ${packages}` } else if (packageManager === 'pipx') { for (let pkg of packages) { await $`pipx install ${pkg}` } } else if (packageManager === 'pkg') { } else if (packageManager === 'port') { for (let pkg of packages) { await $`sudo port install ${pkg}` } } else if (packageManager === 'scoop') { for (let pkg of packages) { await $`scoop install ${pkg}` } } else if (packageManager === 'snap') { for (let pkg of packages) { await $`sudo snap install -y ${pkg}` } } else if (packageManager === 'whalebrew') { for (let pkg of packages) { await $`whalebrew install ${pkg}` } } else if (packageManager === 'winget') { } else if (packageManager === 'yay') { for (let pkg of packages) { await $`yay -Sy --noconfirm --needed ${pkg}` } } else if (packageManager === 'zypper') { await $`sudo zypper install -y ${packages}` } } catch (e) { await $`logg error 'Possibly encountered an error while installing via \`${packageManager}\`'` await $`logg info 'Error was encountered while installing: ${pkg}'` await $`logg info 'Proceeding with the installation..'` } } // main process async function main() { await $`logg info 'Fetching the latest version of the installation map'` installData = await downloadInstallData(); await $`logg info 'Calculating the install orders'` await generateInstallOrders(); await $`logg info 'Ensuring any package managers that will be used are installed / configured'` const packageManagers = Object.keys(installOrders); for (const packageManager of packageManagers) { await ensurePackageManager(packageManager); } await $`logg info 'The install orders were generated:'` console.log(installOrders) await $`logg info 'Running package manager pre-installation steps'` for (const packageManager of packageManagers) { await beforeInstall(packageManager); } await $`logg info 'Running package-specific pre-installation steps'` for (const script of installOrdersPre) { await $`${script}`; } await $`logg info 'Installing the packages'` for (const packageManager of packageManagers) { const asyncOrders = []; asyncOrders.push( Promise.resolve( installPackageList(packageManager, installOrders[packageManager]) ) ); await Promise.all(asyncOrders); } await $`logg info 'Running package-specific post-installation steps'` for (const script of installOrdersPost) { await $`${script}`; } await $`logg info 'Running package manager post-installation steps'` for (const packageManager of packageManagers) { await afterInstall(packageManager); } await $`logg success 'Done!'` } // Start the main process await main();