install.fairie/home/dot_local/bin/executable_installx
Brian Zalewski 6e97ef4274 Latest
2024-01-15 07:20:22 +00:00

340 lines
14 KiB
Text

#!/usr/bin/env zx
import osInfo from 'linux-os-info'
// $.verbose = false
let installOrder, osArch, osId, osType, pkgs, sysType
const cacheDir = os.homedir() + '/.cache/installx'
async function getOsInfo() {
return osInfo({ mode: 'sync' })
}
async function runSilentCommand(command) {
require('child_process').execSync(`${command}`, { stdio: 'inherit', shell: true })
}
async function runScript(key, script) {
fs.writeFileSync(`${cacheDir}/${key}`, script)
const file = await $`cat ${cacheDir}/${key} | grep "^# @file" | sed 's/^# @file //'`
const brief = await $`cat ${cacheDir}/${key} | grep "^# @brief" | sed 's/^# @brief //'`
fs.writeFileSync(`${cacheDir}/${key}-glow`, "# " + file.stdout + "\n> " + brief.stdout + "\n```sh\n" + script + "\n```")
runSilentCommand(`glow "${cacheDir}/${key}-glow" && bash "${cacheDir}/${key}"`)
}
function getPkgData(pref, pkg, installer) {
if (installer) {
if (pkg[`${pref}:${installer}:${osId}:${osArch}`]) {
return `${pref}:${installer}:${osId}:${osArch}` // Handles case like `_bin:pipx:debian:x64:`
} else if (pkg[`${pref}:${osId}:${installer}:${osArch}`]) {
return `${pref}:${osId}:${installer}:${osArch}` // Handles case like `_bin:debian:pipx:x64:`
} else if (pkg[`${pref}:${installer}:${osType}:${osArch}`]) {
return `${pref}:${installer}:${osType}:${osArch}` // Handles case like `_bin:pipx:windows:x64:`
} else if (pkg[`${pref}:${osType}:${installer}:${osArch}`]) {
return `${pref}:${osType}:${installer}:${osArch}` // Handles case like `_bin:windows:pipx:x64:`
} else if (pkg[`${pref}:${installer}:${osId}`]) {
return `${pref}:${installer}:${osType}` // Handles case like `_bin:pipx:fedora:`
} else if (pkg[`${pref}:${osId}:${installer}`]) {
return `${pref}:${osType}:${installer}` // Handles case like `_bin:fedora:pipx:`
} else if (pkg[`${pref}:${installer}:${osType}`]) {
return `${pref}:${installer}:${osType}` // Handles case like `_bin:pipx:darwin:`
} else if (pkg[`${pref}:${osType}:${installer}`]) {
return `${pref}:${osType}:${installer}` // Handles case like `_bin:darwin:pipx:`
} else if (pkg[`${pref}:${installer}`]) {
return `${pref}` // Handles case like `_bin:pipx:`
} else if (pkg[`${pref}`]) {
return `${pref}` // Handles case like `_bin:`
} else {
return false
}
} else {
if (pkg[`${pref}:${osId}:${osArch}`]) {
return `${pref}:${osId}:${osArch}` // Handles case like `pipx:debian:x64:`
} else if (pkg[`${pref}:${osType}:${osArch}`]) {
return `${pref}:${osType}:${osArch}` // Handles case like `pipx:windows:x64:`
} else if (pkg[`${pref}:${osId}`]) {
return `${pref}:${osType}` // Handles case like `pipx:fedora:`
} else if (pkg[`${pref}:${osType}`]) {
return `${pref}:${osType}` // Handles case like `pipx:darwin:`
} else if (pkg[`${pref}`]) {
return `${pref}` // Handles case like `pipx:`
} else {
return false
}
}
}
async function getSoftwareDefinitions() {
try {
return YAML.parse(fs.readFileSync(`${os.homedir()}/.local/share/chezmoi/software.yml`, 'utf8'))
} catch (e) {
throw Error('Failed to load software definitions', e)
}
}
async function getSystemType() {
if (process.platform === "win32") {
return "windows"
} else if (process.platform === "linux") {
if (which.sync('apk')) {
return "apk"
} else if (which.sync('apt-get')) {
return "apt"
} else if (which.sync('dnf')) {
return "dnf"
} else if (which.sync('pacman')) {
return "pacman"
} else if (which.sync('zypper')) {
return "zypper"
} else {
return "linux"
}
} else {
return process.platform
}
}
function expandDeps(keys) {
for (const i of keys) {
for (const pref of installOrder[sysType]) {
const installKey = getPkgData(pref, pkgs[i], false)
if (installKey) {
const installType = installKey.split(':')[0]
const depsKey = getPkgData('_deps', pkgs[i], installType)
if (depsKey) {
const deps = typeof pkgs[i][depsKey] === 'string' ? [pkgs[i][depsKey]] : pkgs[i][depsKey]
return [...keys, ...expandDeps(deps)]
}
}
}
return [...keys]
}
return [...keys]
}
async function bundleInstall(brews, casks) {
const lines = []
for (const cask of casks) {
lines.push(`cask "${cask.cask}"`)
}
for (const brew of brews) {
lines.push(`brew "${brew.brew}"`)
}
fs.writeFileSync('Brewfile', lines.join('\n'))
await $`brew bundle --file Brewfile`
}
async function forEachSeries(iterable) {
for (const x of iterable) {
await x
}
}
async function installPackages(pkgInstructions) {
const combined = {}
const promises = []
for (const option of installOrder[sysType]) {
const instructions = pkgInstructions.filter(x => x.installType === option)
if (instructions.length) {
combined[option] = instructions
}
}
if ((combined.brew && combined.brew.length) || (combined.cask && combined.cask.length)) {
promises.push(bundleInstall(combined.brew ? combined.brew : [], combined.cask ? combined.cask : []))
}
for (const key of Object.keys(combined)) {
switch(key) {
case 'ansible':
promises.push(forEachSeries(combined[key].flatMap(x => x.installList.flatMap(i => $`${key} 127.0.0.1 -v${process.env.DEBUG && 'vv'} -e '{ ansible_connection: "local", ansible_become_user: "root", ansible_user: "${process.env.USER}", ansible_family: "${osId.charAt(0).toUpperCase() + osId.slice(1)}", install_homebrew: False }' -m include_role -a name=${i}`))))
case 'apk':
promises.push($`sudo ${key} add ${combined[key].flatMap(x => x.installList).split(' ')}`)
case 'appimage':
promises.push(...combined[key].flatMap(x => x.installList.flatMap(i => {
if (x.substring(0, 4) === 'http') {
return $`zap install --select-first -q --from ${i}`
} else if ((x.match(/\//g) || []).length === 1) {
return $`zap install --select-first -q --github --from ${i}`
} else {
return $`zap install --select-first -q ${i}`
}
})))
case 'apt':
promises.push($`DEBIAN_FRONTEND=noninteractive sudo apt-get -o DPkg::Options::=--force-confdef install -y ${combined[key].flatMap(x => x.installList).split(' ')}`)
case 'basher':
case 'baulk':
case 'cargo':
case 'crew':
case 'gem':
case 'go':
case 'npm':
case 'pip':
case 'pipx':
case 'scoop': // Maybe needs forEachSeries
case 'winget': // Maybe needs forEachSeries
promises.push(...combined[key].flatMap(x => x.installList.flatMap(i => $`${key} install ${i}`)))
break;
case 'binary':
// TODO
promises.push(...combined[key].flatMap(x => x.installList.flatMap(i => $`TMP="$(mktemp)" && curl -sSL ${i} > "$TMP" && sudo mv "$TMP" /usr/local/src/${x._bin} && chmod +x /usr/local/src/${x._bin}`)))
case 'brew':
case 'cask': // Handled above
break;
case 'choco':
promises.push($`${key} install -y ${combined[key].flatMap(x => x.installList).split(' ')}`)
case 'dnf':
case 'yum':
case 'zypper':
promises.push($`sudo ${key} install -y ${combined[key].flatMap(x => x.installList).split(' ')}`)
break;
case 'emerge':
case 'pkg_add':
promises.push($`sudo ${key} ${combined[key].flatMap(x => x.installList).split(' ')}`)
break;
case 'eopkg':
case 'pkg-freebsd':
case 'pkg-termux':
case 'pkgin':
case 'port':
case 'snap': // TODO - snap testing.. combine with snap-classic and add appropriate logic
promises.push($`sudo ${key === 'pkg-freebsd' || key === 'pkg-termux' ? 'pkg' : key} install ${combined[key].flatMap(x => x.installList).split(' ')}`)
break;
case 'flatpak':
promises.push(forEachSeries(combined[key].flatMap(x => x.installList.flatMap(i => $`sudo ${key} install -y flathub ${i}`))))
break;
case 'github': // TODO
break;
case 'nix-env': // TODO
case 'nix-pkg': // TODO
case 'nix-shell': // TODO
break;
case 'pacman':
promises.push($`sudo ${key} -Sy --noconfirm --needed ${combined[key].flatMap(x => x.installList).split(' ')}`)
break;
case 'pkg-darwin':
break;
case 'sbopkg': // TODO
break;
case 'script':
promises.push(...combined[key].flatMap(x => x.installList.map(i => $`${i}`)))
break;
case 'snap-classic':
promises.push($`sudo snap install --classic ${combined[key].flatMap(x => x.installList).split(' ')}`)
break;
case 'whalebrew': // TODO
break;
case 'xbps':
promises.push($`sudo xbps-install -S ${combined[key].flatMap(x => x.installList).split(' ')}`)
break;
case 'yay':
promises.push($`yay -Sy --noconfirm --needed ${combined[key].flatMap(x => x.installList).split(' ')}`)
break;
default:
console.log(`Unable to find install key instructions for ${key}`)
}
}
await Promise.all(promises)
}
async function acquireManagerList(type, command) {
if (fs.existsSync(`${cacheDir}/${type}`)) {
setTimeout(() => {
require('child_process').execSync(`${command} > ${cacheDir}/${type}`)
}, 0)
} else {
require('child_process').execSync(`${command} > ${cacheDir}/${type}`)
}
return fs.readFileSync(`${cacheDir}/${type}`).toString().split('\n')
}
async function main() {
await $`mkdir -p ${cacheDir}`
const initData = await Promise.all([
getOsInfo(),
getSoftwareDefinitions(),
getSystemType()
])
osArch = initData[0].arch
osId = process.platform === 'win32' ? 'win32' : (process.platform === 'linux' ? initData[0].id : process.platform)
osType = process.platform === 'win32' ? 'windows' : process.platform
pkgs = initData[1].softwarePackages
sysType = initData[2]
installOrder = initData[1].installerPreference
const lists = [
acquireManagerList('brew', `brew list -1`),
acquireManagerList('cargo', `cargo install --list | awk '/^[[:alnum:]]/ {print $1}'`),
acquireManagerList('gem', `gem list | awk '{print $1}'`),
acquireManagerList('npm', `volta list --format plain | awk '{print $2}' | sed 's/@.*//'`),
acquireManagerList('pip3', `pip3 list | awk '{print $1}'`),
acquireManagerList('pipx', `pipx list --short | awk '{print $1}'`)
]
const managerLists = {
brew: lists[0],
cargo: lists[1],
gem: lists[2],
npm: lists[3],
pip3: lists[4],
pipx: lists[5]
}
const installKeys = Object.keys(pkgs)
.filter(i => expandDeps(argv._).includes(i))
const installData = installKeys
.map(i => {
for (const pref of installOrder[sysType]) {
const installKey = getPkgData(pref, pkgs[i], false)
if (installKey) {
return {
...pkgs[i],
listKey: i,
installKey,
installType: installKey.split(':')[0],
installList: typeof pkgs[i][installKey] === 'string' ? [pkgs[i][installKey]] : pkgs[i][installKey]
}
}
}
return {
...pkgs[i],
listKey: i,
installKey: false,
installType: false,
installList: []
}
})
.filter(x => x.installKey)
const installInstructions = installData
.filter(x => {
// Filter out packages already installed by by package managers
return Object.keys(managerLists).includes(x.installType)
})
.filter(x => {
// Filter out macOS apps that already have a _app installed
if (x.installType === 'cask') {
const appField = getPkgData('_app', x, x.installType)
if (fs.existsSync(`/Applications/${x[appField]}`) || fs.existsSync(`${os.homedir()}/Applications/${x[appField]}`)) {
return false
}
}
return true
})
.filter(async x => {
// Filter out packages that already have a bin in the PATH
const binField = getPkgData('_bin', x, x.installType)
const binCheck = x[binField] && await which(x[binField], { nothrow: true })
return binField ? binCheck : true
})
.filter(async x => {
// Filter out packages that do not pass _when check
const whenField = getPkgData('_when', x, x.installType)
const whenCheck = x[whenField] && await $`${x[whenField]}`.exitCode == 0
return whenField ? whenCheck : true
})
await installPackages(installInstructions)
const postScripts = installData
.flatMap(x => {
const postField = getPkgData('_post', x, x.installType)
return (postField && runScript(x.listKey, x[postField])) || Promise.resolve()
})
await Promise.all(postScripts)
}
main()