2024-01-13 21:48:00 -08:00
|
|
|
#!/usr/bin/env zx
|
|
|
|
import osInfo from 'linux-os-info'
|
|
|
|
|
2024-04-01 01:56:27 -07:00
|
|
|
$.verbose = false
|
2024-01-14 23:20:22 -08:00
|
|
|
|
2024-01-14 00:40:00 -08:00
|
|
|
let installOrder, osArch, osId, osType, pkgs, sysType
|
2024-01-14 23:20:22 -08:00
|
|
|
const cacheDir = os.homedir() + '/.cache/installx'
|
2024-01-13 21:48:00 -08:00
|
|
|
|
2024-01-15 20:43:57 -08:00
|
|
|
function log(message) {
|
2024-05-03 19:40:44 -07:00
|
|
|
console.log(`${chalk.cyanBright('instx->')} ${message}`)
|
2024-01-15 20:43:57 -08:00
|
|
|
}
|
|
|
|
|
2024-01-13 21:48:00 -08:00
|
|
|
async function getOsInfo() {
|
|
|
|
return osInfo({ mode: 'sync' })
|
|
|
|
}
|
|
|
|
|
2024-01-15 20:43:57 -08:00
|
|
|
function execPromise(command) {
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
|
|
require('child_process').exec(command, (error, stdout, stderr) => {
|
|
|
|
if (error) {
|
|
|
|
reject(error)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
resolve(stdout.trim())
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-14 23:20:22 -08:00
|
|
|
async function runSilentCommand(command) {
|
2024-04-01 04:34:07 -07:00
|
|
|
return require('child_process').execSync(`${command}`, { stdio: 'inherit', shell: true })
|
2024-01-15 20:43:57 -08:00
|
|
|
}
|
2024-01-14 23:20:22 -08:00
|
|
|
|
|
|
|
async function runScript(key, script) {
|
2024-04-01 01:56:27 -07:00
|
|
|
log(`Running script..`)
|
2024-04-01 04:34:07 -07:00
|
|
|
fs.writeFileSync(`${cacheDir}/${key}-raw`, script)
|
2024-05-04 22:50:59 -07:00
|
|
|
const [templatedScript, file, brief] = await Promise.all([
|
2024-04-01 04:34:07 -07:00
|
|
|
$`cat "${cacheDir}/${key}-raw" | chezmoi execute-template`,
|
|
|
|
$`cat "${cacheDir}/${key}-raw" | ( grep "^# @file" || [ "$?" == "1" ] ) | sed 's/^# @file //'`,
|
|
|
|
$`cat "${cacheDir}/${key}-raw" | ( grep "^# @brief" || [ "$?" == "1" ] ) | sed 's/^# @brief //'`
|
2024-04-01 01:56:27 -07:00
|
|
|
])
|
2024-04-01 04:34:07 -07:00
|
|
|
fs.writeFileSync(`${cacheDir}/${key}-glow`, (file.stdout ? `# ${file.stdout}\n\n` : '') + (brief.stdout ? `> ${brief.stdout}\n\n` : '') + '```sh\n' + templatedScript.stdout + "\n```")
|
|
|
|
fs.writeFileSync(`${cacheDir}/${key}`, templatedScript.stdout)
|
2024-04-01 01:56:27 -07:00
|
|
|
try {
|
2024-05-03 22:11:59 -07:00
|
|
|
runSilentCommand(`glow --width 80 "${cacheDir}/${key}-glow"`)
|
2024-04-02 01:07:20 -07:00
|
|
|
// TODO: Set process.env.DEBUG || true here because the asynchronous method is not logging properly / running slow
|
2024-05-04 21:05:33 -07:00
|
|
|
if (process.env.DEBUG) {
|
2024-04-02 01:07:20 -07:00
|
|
|
return runSilentCommand(`bash "${cacheDir}/${key}" || logg error 'Error occurred while processing script for ${key}'`)
|
2024-04-01 04:34:07 -07:00
|
|
|
} else {
|
|
|
|
return $`bash "${cacheDir}/${key}" || logg error 'Error occurred while processing script for ${key}'`.pipe(process.stdout)
|
|
|
|
}
|
2024-04-01 01:56:27 -07:00
|
|
|
} catch (e) {
|
|
|
|
console.error(`Failed to run script associated with ${key}`, e)
|
|
|
|
}
|
2024-01-14 23:20:22 -08:00
|
|
|
}
|
|
|
|
|
2024-01-14 00:40:00 -08:00
|
|
|
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}`]) {
|
2024-04-01 01:56:27 -07:00
|
|
|
return `${pref}:${installer}` // Handles case like `_bin:pipx:`
|
2024-01-14 00:40:00 -08:00
|
|
|
} 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
|
|
|
|
}
|
2024-01-13 21:48:00 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-14 00:40:00 -08:00
|
|
|
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]
|
|
|
|
}
|
|
|
|
|
2024-05-03 22:11:59 -07:00
|
|
|
async function createCaskLinks(caskMap) {
|
2024-05-04 22:50:59 -07:00
|
|
|
if (!caskMap) return
|
2024-05-03 22:11:59 -07:00
|
|
|
const caskApps = caskMap
|
2024-01-15 20:43:57 -08:00
|
|
|
.filter(x => {
|
|
|
|
// Filter out macOS apps that already have a _app installed
|
|
|
|
if (x.installType === 'cask' || (osId === 'darwin' && x._app)) {
|
|
|
|
const appField = getPkgData('_app', x, x.installType)
|
|
|
|
const binField = getPkgData('_bin', x, x.installType)
|
|
|
|
const sysDir = fs.existsSync(`/Applications/${x[appField]}`)
|
|
|
|
const homeDir = fs.existsSync(`${os.homedir()}/Applications/${x[appField]}`)
|
|
|
|
const binFile = fs.existsSync(`${os.homedir()}/.local/bin/cask/${x[binField]}`)
|
2024-05-03 22:11:59 -07:00
|
|
|
if (sysDir || homeDir) {
|
|
|
|
return !binFile
|
|
|
|
} else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return false
|
2024-01-15 20:43:57 -08:00
|
|
|
}
|
|
|
|
})
|
|
|
|
caskApps.length && await $`mkdir -p "$HOME/.local/bin/cask"`
|
|
|
|
for (const app of caskApps) {
|
|
|
|
const appField = getPkgData('_app', app, app.installType)
|
2024-05-03 22:11:59 -07:00
|
|
|
if (!appField) {
|
|
|
|
log(`${app.listKey} is missing an _app definition`)
|
|
|
|
return
|
|
|
|
}
|
2024-01-15 20:43:57 -08:00
|
|
|
const binField = getPkgData('_bin', app, app.installType)
|
2024-05-03 22:11:59 -07:00
|
|
|
if (!binField) {
|
|
|
|
log(`${app.listKey} is missing a _bin definition`)
|
|
|
|
return
|
|
|
|
}
|
2024-01-28 21:13:53 -08:00
|
|
|
if (fs.existsSync(`${os.homedir()}/Applications/${app[appField]}`)) {
|
|
|
|
fs.writeFileSync(`${os.homedir()}/.local/bin/cask/${app[binField]}`, `#!/usr/bin/env bash\nopen "$HOME/Applications/${app[appField]}" $*`)
|
|
|
|
await $`chmod +x '${os.homedir()}/.local/bin/cask/${app[binField]}'`
|
|
|
|
} else if (fs.existsSync(`/Applications/${app[appField]}`)) {
|
|
|
|
fs.writeFileSync(`${os.homedir()}/.local/bin/cask/${app[binField]}`, `#!/usr/bin/env bash\nopen "/Applications/${app[appField]}" $*`)
|
|
|
|
await $`chmod +x '${os.homedir()}/.local/bin/cask/${app[binField]}'`
|
2024-01-15 20:43:57 -08:00
|
|
|
} else {
|
2024-01-28 21:13:53 -08:00
|
|
|
log(`Unable to create bin link to ${app[appField]}`)
|
2024-01-15 20:43:57 -08:00
|
|
|
}
|
2024-01-14 00:40:00 -08:00
|
|
|
}
|
2024-01-15 20:43:57 -08:00
|
|
|
caskApps.length && log(`Finished creating Homebrew cask links in ~/.local/bin/cask`)
|
|
|
|
}
|
|
|
|
|
2024-05-03 22:11:59 -07:00
|
|
|
async function createFlatpakLinks(flatpakMap) {
|
2024-01-28 21:13:53 -08:00
|
|
|
const flatpakInstallations = await $`flatpak --installations`
|
|
|
|
const flatpakDir = flatpakInstallations.stdout.replace('\n', '')
|
2024-05-03 22:11:59 -07:00
|
|
|
const flatpakApps = flatpakMap
|
2024-01-28 21:13:53 -08:00
|
|
|
.filter(x => {
|
|
|
|
if (x.installType === 'flatpak') {
|
|
|
|
const binField = getPkgData('_bin', x, x.installType)
|
|
|
|
const binFile = fs.existsSync(`${os.homedir()}/.local/bin/flatpak/${x[binField]}`)
|
|
|
|
return !binFile
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
flatpakApps.length && await $`mkdir -p "$HOME/.local/bin/flatpak"`
|
|
|
|
for (const app of flatpakApps) {
|
|
|
|
const binField = getPkgData('_bin', app, app.installType)
|
2024-05-03 22:11:59 -07:00
|
|
|
if (!binField) {
|
|
|
|
log(`${app.listKey} is missing a _bin definition`)
|
|
|
|
return
|
|
|
|
}
|
2024-01-28 21:13:53 -08:00
|
|
|
if (fs.existsSync(`${flatpakDir}/app/${app.installList[0]}`)) {
|
|
|
|
fs.writeFileSync(`${os.homedir()}/.local/bin/flatpak/${app[binField]}`, `#!/usr/bin/env bash\nflatpak run ${app.installList[0]} $*`)
|
|
|
|
await $`chmod +x '${os.homedir()}/.local/bin/flatpak/${app[binField]}'`
|
|
|
|
} else {
|
|
|
|
log(`Unable to create bin link to ${x.flatpak}`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
flatpakApps.length && log(`Finished creating Flatpak links in ~/.local/bin/flatpak`)
|
|
|
|
}
|
|
|
|
|
2024-05-03 22:11:59 -07:00
|
|
|
async function bundleInstall(brews, casks, caskMap) {
|
2024-01-15 20:43:57 -08:00
|
|
|
try {
|
|
|
|
const lines = []
|
2024-05-03 22:11:59 -07:00
|
|
|
casks.length && log(`Adding following casks to Brewfile for installation: ${casks.join(' ')}`)
|
2024-01-15 20:43:57 -08:00
|
|
|
for (const cask of casks) {
|
2024-05-03 22:11:59 -07:00
|
|
|
if (cask.indexOf('/') !== -1) {
|
|
|
|
lines.push(`tap "${cask.substring(0, cask.lastIndexOf('/'))}"`)
|
|
|
|
}
|
2024-05-04 00:25:19 -07:00
|
|
|
if (cask.indexOf(' --no-quarantine') === -1) {
|
|
|
|
lines.push(`cask "${cask}"`)
|
|
|
|
} else {
|
|
|
|
lines.push(`cask "${cask.replace(' --no-quarantine', '')}", args: { "no-quarantine": true }`)
|
|
|
|
}
|
2024-01-15 20:43:57 -08:00
|
|
|
}
|
2024-05-03 22:11:59 -07:00
|
|
|
brews.length && log(`Adding following brews to Brewfile for installation: ${brews.join(' ')}`)
|
2024-01-15 20:43:57 -08:00
|
|
|
for (const brew of brews) {
|
2024-05-03 22:11:59 -07:00
|
|
|
if (brew.indexOf('/') !== -1) {
|
|
|
|
lines.push(`tap "${brew.substring(0, brew.lastIndexOf('/'))}"`)
|
|
|
|
}
|
2024-01-15 20:43:57 -08:00
|
|
|
lines.push(`brew "${brew}"`)
|
|
|
|
}
|
|
|
|
log(`Creating Brewfile to install from`)
|
2024-05-04 22:50:59 -07:00
|
|
|
cd(await $`mktemp -d`)
|
2024-01-15 20:43:57 -08:00
|
|
|
fs.writeFileSync('Brewfile', lines.join('\n'))
|
|
|
|
log(`Installing packages via brew bundle`)
|
|
|
|
await $`brew bundle --file Brewfile`
|
|
|
|
log(`Finished installing via Brewfile`)
|
|
|
|
} catch (e) {
|
2024-05-03 22:11:59 -07:00
|
|
|
console.log('Error:', e)
|
2024-01-15 20:43:57 -08:00
|
|
|
log(`Error occurred while installing via Brewfile`)
|
2024-01-14 00:40:00 -08:00
|
|
|
}
|
2024-05-04 02:28:17 -07:00
|
|
|
|
|
|
|
try {
|
|
|
|
await createCaskLinks(caskMap)
|
|
|
|
} catch (e) {
|
|
|
|
console.log('Error:', e)
|
|
|
|
log(`Error occurred while creating cask bin links`)
|
|
|
|
}
|
2024-01-14 00:40:00 -08:00
|
|
|
}
|
|
|
|
|
2024-01-14 23:20:22 -08:00
|
|
|
async function forEachSeries(iterable) {
|
|
|
|
for (const x of iterable) {
|
|
|
|
await x
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-14 00:40:00 -08:00
|
|
|
async function installPackages(pkgInstructions) {
|
|
|
|
const combined = {}
|
|
|
|
const promises = []
|
2024-01-15 20:43:57 -08:00
|
|
|
log(`Populating install order lists`)
|
2024-01-14 00:40:00 -08:00
|
|
|
for (const option of installOrder[sysType]) {
|
2024-01-14 23:20:22 -08:00
|
|
|
const instructions = pkgInstructions.filter(x => x.installType === option)
|
|
|
|
if (instructions.length) {
|
|
|
|
combined[option] = instructions
|
2024-01-14 00:40:00 -08:00
|
|
|
}
|
|
|
|
}
|
2024-01-15 20:43:57 -08:00
|
|
|
log(`Running Homebrew installation via Brewfile`)
|
2024-01-14 23:20:22 -08:00
|
|
|
if ((combined.brew && combined.brew.length) || (combined.cask && combined.cask.length)) {
|
2024-05-03 22:11:59 -07:00
|
|
|
promises.push(bundleInstall(combined.brew ? combined.brew.flatMap(x => x.installList.flatMap(i => i)) : [], combined.cask ? combined.cask.flatMap(x => x.installList.flatMap(i => i)) : [], combined.cask))
|
2024-01-14 00:40:00 -08:00
|
|
|
}
|
2024-01-14 23:20:22 -08:00
|
|
|
for (const key of Object.keys(combined)) {
|
2024-01-15 21:04:34 -08:00
|
|
|
if (key !== 'script') {
|
|
|
|
log(`Install orders for ${key}: ${combined[key].flatMap(i => i.installList).join(' ')}`)
|
|
|
|
}
|
2024-01-15 20:43:57 -08:00
|
|
|
switch (key) {
|
2024-01-14 23:20:22 -08:00
|
|
|
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}`))))
|
2024-01-28 21:13:53 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'apk':
|
|
|
|
promises.push($`sudo ${key} add ${combined[key].flatMap(x => x.installList).split(' ')}`)
|
2024-04-01 01:56:27 -07:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
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}`
|
|
|
|
}
|
|
|
|
})))
|
2024-01-28 21:13:53 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'apt':
|
|
|
|
promises.push($`DEBIAN_FRONTEND=noninteractive sudo apt-get -o DPkg::Options::=--force-confdef install -y ${combined[key].flatMap(x => x.installList).split(' ')}`)
|
2024-01-28 21:13:53 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'basher':
|
|
|
|
case 'baulk':
|
|
|
|
case 'cargo':
|
|
|
|
case 'crew':
|
|
|
|
case 'gem':
|
|
|
|
case 'go':
|
|
|
|
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}`)))
|
2024-01-15 20:43:57 -08:00
|
|
|
break
|
2024-05-03 22:11:59 -07:00
|
|
|
case 'npm':
|
|
|
|
promises.push(...combined[key].flatMap(x => x.installList.flatMap(i => $`${key} install -g ${i}`)))
|
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
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}`)))
|
2024-01-28 21:13:53 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'brew':
|
|
|
|
case 'cask': // Handled above
|
2024-01-15 20:43:57 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'choco':
|
2024-01-15 20:43:57 -08:00
|
|
|
promises.push($`${key} install -y ${combined[key].flatMap(x => x.installList).join(' ')}`)
|
2024-01-28 21:13:53 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'dnf':
|
|
|
|
case 'yum':
|
|
|
|
case 'zypper':
|
2024-01-15 20:43:57 -08:00
|
|
|
promises.push($`sudo ${key} install -y ${combined[key].flatMap(x => x.installList).join(' ')}`)
|
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'emerge':
|
|
|
|
case 'pkg_add':
|
2024-01-15 20:43:57 -08:00
|
|
|
promises.push($`sudo ${key} ${combined[key].flatMap(x => x.installList).join(' ')}`)
|
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
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
|
2024-01-15 20:43:57 -08:00
|
|
|
promises.push($`sudo ${key === 'pkg-freebsd' || key === 'pkg-termux' ? 'pkg' : key} install ${combined[key].flatMap(x => x.installList).join(' ')}`)
|
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'flatpak':
|
|
|
|
promises.push(forEachSeries(combined[key].flatMap(x => x.installList.flatMap(i => $`sudo ${key} install -y flathub ${i}`))))
|
2024-01-15 20:43:57 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'github': // TODO
|
2024-01-15 20:43:57 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'nix-env': // TODO
|
|
|
|
case 'nix-pkg': // TODO
|
|
|
|
case 'nix-shell': // TODO
|
2024-01-15 20:43:57 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'pacman':
|
2024-01-15 20:43:57 -08:00
|
|
|
promises.push($`sudo ${key} -Sy --noconfirm --needed ${combined[key].flatMap(x => x.installList).join(' ')}`)
|
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'pkg-darwin':
|
2024-01-15 20:43:57 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'sbopkg': // TODO
|
2024-01-15 20:43:57 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'script':
|
2024-04-01 01:56:27 -07:00
|
|
|
promises.push(...combined[key].flatMap(x => x.installList.flatMap(i => runScript(x.listKey, i))))
|
2024-01-15 20:43:57 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'whalebrew': // TODO
|
2024-01-15 20:43:57 -08:00
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'xbps':
|
2024-01-15 20:43:57 -08:00
|
|
|
promises.push($`sudo xbps-install -S ${combined[key].flatMap(x => x.installList).join(' ')}`)
|
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
case 'yay':
|
2024-01-15 20:43:57 -08:00
|
|
|
promises.push($`yay -Sy --noconfirm --needed ${combined[key].flatMap(x => x.installList).join(' ')}`)
|
|
|
|
break
|
2024-01-14 23:20:22 -08:00
|
|
|
default:
|
2024-04-01 01:56:27 -07:00
|
|
|
log(`Unable to find install key instructions for ${key}`)
|
2024-01-14 00:40:00 -08:00
|
|
|
}
|
|
|
|
}
|
2024-05-03 22:11:59 -07:00
|
|
|
log(`Performing ${promises.length} installations`)
|
|
|
|
process.env.DEBUG && console.log('Queued installs:', promises)
|
2024-01-15 20:43:57 -08:00
|
|
|
const installs = await Promise.allSettled(promises)
|
|
|
|
log(`All of the installations have finished`)
|
2024-05-03 22:11:59 -07:00
|
|
|
process.env.DEBUG && console.log('Completed installs:', installs)
|
2024-01-28 21:13:53 -08:00
|
|
|
await postInstall(combined)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function postInstall(combined) {
|
|
|
|
log(`Running post-install routine`)
|
|
|
|
const promises = []
|
2024-05-03 22:11:59 -07:00
|
|
|
Object.keys(combined).includes('flatpak') && promises.push(createFlatpakLinks(combined.flatpak))
|
2024-01-28 21:13:53 -08:00
|
|
|
const postInstalls = await Promise.allSettled(promises)
|
2024-04-01 01:56:27 -07:00
|
|
|
process.env.DEBUG && console.log('Post installs:', postInstalls)
|
2024-01-14 23:20:22 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
async function acquireManagerList(type, command) {
|
2024-01-28 21:13:53 -08:00
|
|
|
if (which.sync(type, { nothrow: true })) {
|
|
|
|
if (fs.existsSync(`${cacheDir}/${type}`)) {
|
|
|
|
setTimeout(() => {
|
2024-04-01 04:34:07 -07:00
|
|
|
require('child_process').exec(`${command} > ${cacheDir}/${type}`)
|
2024-01-28 21:13:53 -08:00
|
|
|
}, 0)
|
|
|
|
} else {
|
2024-01-14 23:20:22 -08:00
|
|
|
require('child_process').execSync(`${command} > ${cacheDir}/${type}`)
|
2024-01-28 21:13:53 -08:00
|
|
|
}
|
|
|
|
return fs.readFileSync(`${cacheDir}/${type}`).toString().split('\n')
|
2024-01-14 23:20:22 -08:00
|
|
|
} else {
|
2024-01-28 21:13:53 -08:00
|
|
|
return []
|
2024-01-14 00:40:00 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-15 20:43:57 -08:00
|
|
|
function pkgMap(pkgDefs) {
|
|
|
|
return pkgDefs
|
|
|
|
.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)
|
|
|
|
}
|
|
|
|
|
2024-01-13 21:48:00 -08:00
|
|
|
|
|
|
|
async function main() {
|
2024-01-14 23:20:22 -08:00
|
|
|
await $`mkdir -p ${cacheDir}`
|
2024-01-15 20:43:57 -08:00
|
|
|
log(`Acquiring software definitions and system information`)
|
2024-01-14 00:40:00 -08:00
|
|
|
const initData = await Promise.all([
|
|
|
|
getOsInfo(),
|
|
|
|
getSoftwareDefinitions(),
|
2024-01-14 23:20:22 -08:00
|
|
|
getSystemType()
|
2024-01-14 00:40:00 -08:00
|
|
|
])
|
|
|
|
osArch = initData[0].arch
|
2024-01-13 21:48:00 -08:00
|
|
|
osId = process.platform === 'win32' ? 'win32' : (process.platform === 'linux' ? initData[0].id : process.platform)
|
|
|
|
osType = process.platform === 'win32' ? 'windows' : process.platform
|
2024-01-14 00:40:00 -08:00
|
|
|
pkgs = initData[1].softwarePackages
|
|
|
|
sysType = initData[2]
|
|
|
|
installOrder = initData[1].installerPreference
|
2024-01-15 20:43:57 -08:00
|
|
|
log(`Populating lists of pre-installed packages`)
|
2024-05-03 22:11:59 -07:00
|
|
|
const listPromises = [
|
2024-04-01 01:56:27 -07:00
|
|
|
acquireManagerList('apt', `if command -v dpkg; then dpkg -l; fi`),
|
2024-01-14 23:20:22 -08:00
|
|
|
acquireManagerList('brew', `brew list -1`),
|
|
|
|
acquireManagerList('cargo', `cargo install --list | awk '/^[[:alnum:]]/ {print $1}'`),
|
2024-01-28 21:13:53 -08:00
|
|
|
acquireManagerList('dnf', `rpm -qa`),
|
|
|
|
acquireManagerList('flatpak', `flatpak --columns=app list`),
|
2024-01-14 23:20:22 -08:00
|
|
|
acquireManagerList('gem', `gem list | awk '{print $1}'`),
|
|
|
|
acquireManagerList('npm', `volta list --format plain | awk '{print $2}' | sed 's/@.*//'`),
|
2024-01-28 21:13:53 -08:00
|
|
|
acquireManagerList('pacman', `pacman -Qs`),
|
2024-05-04 21:05:33 -07:00
|
|
|
acquireManagerList('pip', `pip3 list | awk '{print $1}'`),
|
2024-01-28 21:13:53 -08:00
|
|
|
acquireManagerList('pipx', `pipx list --short | awk '{print $1}'`),
|
2024-04-01 01:56:27 -07:00
|
|
|
acquireManagerList('snap', `if command -v snapd; then snap list; fi`),
|
2024-01-28 21:13:53 -08:00
|
|
|
acquireManagerList('zap', `zap list`)
|
2024-01-14 23:20:22 -08:00
|
|
|
]
|
2024-05-03 22:11:59 -07:00
|
|
|
const lists = await Promise.all(listPromises)
|
2024-01-14 00:40:00 -08:00
|
|
|
const managerLists = {
|
2024-01-28 21:13:53 -08:00
|
|
|
appimage: lists[6],
|
|
|
|
apt: lists[0],
|
|
|
|
brew: lists[1],
|
|
|
|
cargo: lists[2],
|
|
|
|
cask: lists[1],
|
|
|
|
dnf: lists[3],
|
|
|
|
flatpak: lists[4],
|
|
|
|
gem: lists[5],
|
|
|
|
npm: lists[6],
|
|
|
|
pacman: lists[7],
|
2024-05-04 21:05:33 -07:00
|
|
|
pip: lists[8],
|
2024-01-28 21:13:53 -08:00
|
|
|
pipx: lists[9],
|
|
|
|
snap: lists[10],
|
|
|
|
zap: lists[11]
|
2024-01-14 00:40:00 -08:00
|
|
|
}
|
2024-01-15 20:43:57 -08:00
|
|
|
log(`Acquiring installation keys`)
|
2024-01-14 23:20:22 -08:00
|
|
|
const installKeys = Object.keys(pkgs)
|
|
|
|
.filter(i => expandDeps(argv._).includes(i))
|
2024-01-15 20:43:57 -08:00
|
|
|
log(`Constructing installation data`)
|
|
|
|
const installData = pkgMap(installKeys)
|
|
|
|
log(`Filtering install instructions`)
|
2024-01-14 23:20:22 -08:00
|
|
|
const installInstructions = installData
|
2024-05-03 22:11:59 -07:00
|
|
|
.map(x => {
|
|
|
|
return {
|
|
|
|
...x,
|
|
|
|
installList: x.installList.filter(y => {
|
|
|
|
if ((x.installType === 'brew' || x.installType === 'cask') && y.includes('/')) {
|
|
|
|
return managerLists[x.installType] ? !managerLists[x.installType].includes(y.substring(y.lastIndexOf('/') + 1, y.length)) : true
|
|
|
|
} else {
|
|
|
|
return managerLists[x.installType] ? !managerLists[x.installType].includes(y) : true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
2024-01-14 00:40:00 -08:00
|
|
|
.filter(x => {
|
|
|
|
// Filter out packages already installed by by package managers
|
2024-05-03 22:11:59 -07:00
|
|
|
return x.installList.length
|
2024-01-14 00:40:00 -08:00
|
|
|
})
|
2024-05-04 21:05:33 -07:00
|
|
|
.filter(x => {
|
|
|
|
// Filter out packages that contain a deprecation note
|
|
|
|
return !x._deprecated
|
|
|
|
})
|
2024-01-14 00:40:00 -08:00
|
|
|
.filter(x => {
|
|
|
|
// Filter out macOS apps that already have a _app installed
|
2024-01-15 20:43:57 -08:00
|
|
|
if (x.installType === 'cask' || (osId === 'darwin' && x._app)) {
|
2024-01-14 00:40:00 -08:00
|
|
|
const appField = getPkgData('_app', x, x.installType)
|
2024-05-03 22:11:59 -07:00
|
|
|
const appCheck = fs.existsSync(`/Applications/${x[appField]}`) || fs.existsSync(`${os.homedir()}/Applications/${x[appField]}`)
|
|
|
|
appCheck && log(`Skipping installation of ${x.listKey} because the application is in an Applications folder`)
|
|
|
|
return !appCheck
|
|
|
|
} else {
|
|
|
|
return true
|
2024-01-14 00:40:00 -08:00
|
|
|
}
|
|
|
|
})
|
2024-01-15 20:43:57 -08:00
|
|
|
.filter(x => {
|
2024-01-14 00:40:00 -08:00
|
|
|
// Filter out packages that already have a bin in the PATH
|
|
|
|
const binField = getPkgData('_bin', x, x.installType)
|
2024-01-15 20:43:57 -08:00
|
|
|
const isArray = Array.isArray(x[binField])
|
|
|
|
if (typeof x[binField] === 'string' || isArray) {
|
2024-05-03 22:11:59 -07:00
|
|
|
isArray && log(`_bin field for ${x.listKey} is an array so the first entry will be used to check`)
|
|
|
|
const whichCheck = which.sync(typeof x[binField] === 'string' ? x[binField] : x[binField][0], { nothrow: true })
|
|
|
|
whichCheck && log(`Skipping installation of ${x.listKey} because its binary is available in the PATH`)
|
|
|
|
return !whichCheck
|
|
|
|
} else {
|
|
|
|
log(`Ignoring _bin check because the _bin field for ${x.listKey} is not a string or array`)
|
|
|
|
return true
|
2024-01-15 20:43:57 -08:00
|
|
|
}
|
2024-01-14 00:40:00 -08:00
|
|
|
})
|
2024-01-15 20:43:57 -08:00
|
|
|
.filter(x => {
|
2024-01-14 00:40:00 -08:00
|
|
|
// Filter out packages that do not pass _when check
|
|
|
|
const whenField = getPkgData('_when', x, x.installType)
|
2024-01-15 20:43:57 -08:00
|
|
|
if (x[whenField]) {
|
|
|
|
if (typeof x[whenField] === 'string') {
|
|
|
|
try {
|
|
|
|
execSync(`${x[whenField]}`)
|
|
|
|
return false
|
|
|
|
} catch (e) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log(`typeof _when for ${x.listKey} must be a string`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
2024-01-13 21:48:00 -08:00
|
|
|
})
|
2024-01-15 20:43:57 -08:00
|
|
|
log(`Running installation routine`)
|
2024-01-14 00:40:00 -08:00
|
|
|
await installPackages(installInstructions)
|
2024-05-04 21:05:33 -07:00
|
|
|
log(`Adding users / groups defined under _groups`)
|
|
|
|
const usersGroupsAdditions = installData
|
|
|
|
.flatMap(x => {
|
|
|
|
const groupsField = getPkgData('_groups', x, x.installType)
|
|
|
|
if (!groupsField) return Promise.resolve()
|
|
|
|
log(`Ensuring user(s) / group(s) created for ${x.listKey}`)
|
|
|
|
if (typeof typeof x[groupsField] !== 'string' && !Array.isArray(x[groupsField])) {
|
|
|
|
log(`Failed to parse _groups for ${x.installKey}. The _groups field must be either a string or string[].`)
|
|
|
|
return Promise.resolve()
|
|
|
|
} else {
|
|
|
|
const groups = typeof x[groupsField] === 'string' ? [x[groupsField]] : x[groupsField]
|
|
|
|
return groups.flatMap(y => {
|
2024-05-04 22:50:59 -07:00
|
|
|
return $`sudo "${os.homedir()}/.local/bin/add-usergroup" "${process.env.USER}" "${y}"`
|
2024-05-04 21:05:33 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
await Promise.allSettled(usersGroupsAdditions)
|
|
|
|
log(`Running post-install inline scripts`)
|
2024-01-15 21:04:34 -08:00
|
|
|
const postScripts = installData
|
2024-01-14 23:20:22 -08:00
|
|
|
.flatMap(x => {
|
|
|
|
const postField = getPkgData('_post', x, x.installType)
|
2024-04-01 04:34:07 -07:00
|
|
|
if (!postField) return Promise.resolve()
|
2024-04-01 01:56:27 -07:00
|
|
|
log(`Running post-install script for ${x.listKey}`)
|
2024-01-14 23:20:22 -08:00
|
|
|
return (postField && runScript(x.listKey, x[postField])) || Promise.resolve()
|
|
|
|
})
|
2024-05-04 21:05:33 -07:00
|
|
|
log(`Running post-install scripts defined in ~/.local/bin/post-installx`)
|
|
|
|
const postScriptFiles = installData
|
|
|
|
.flatMap(x => {
|
2024-05-04 22:50:59 -07:00
|
|
|
const scriptPath = `${os.homedir()}/.local/bin/post-installx/post-${x.listKey}.sh`
|
2024-05-04 21:05:33 -07:00
|
|
|
const scriptExists = fs.existsSync(scriptPath)
|
|
|
|
if (!scriptExists) return Promise.resolve()
|
|
|
|
log(`Running post-install script defined in ${scriptPath}`)
|
|
|
|
return runScript(`post-${x.listKey}.sh`, fs.readFileSync(scriptPath, 'utf8'))
|
|
|
|
})
|
2024-05-04 22:50:59 -07:00
|
|
|
await Promise.allSettled(postScripts.concat(postScriptFiles))
|
2024-05-04 21:05:33 -07:00
|
|
|
log(`Starting services flagged with _serviceEnabled`)
|
|
|
|
const systemctlInstalled = which.sync('systemctl', { nothrow: true })
|
|
|
|
const brewInstalled = which.sync('brew', { nothrow: true })
|
|
|
|
const servicePromises = installData
|
2024-05-04 22:50:59 -07:00
|
|
|
.filter(x => !!x._serviceEnabled)
|
|
|
|
.filter(x => !!x._service)
|
2024-05-04 21:05:33 -07:00
|
|
|
.flatMap(x => {
|
|
|
|
const serviceField = getPkgData('_service', x, x.installType)
|
|
|
|
if (!serviceField) return Promise.resolve()
|
|
|
|
const services = typeof x[serviceField] === 'string' ? [{ name: x[serviceField] }] : (Array.isArray(x[serviceField]) ? x[serviceField] : [{ name: x[serviceField].name, sudo: x[serviceField].sudo }])
|
|
|
|
return services.flatMap(y => {
|
|
|
|
const name = typeof y === 'string' ? y : y.name
|
|
|
|
const sudo = typeof y === 'string' ? null : y.sudo
|
|
|
|
if (osType === 'linux' && x.installType !== 'brew' && x.installType !== 'cask' && systemctlInstalled) {
|
|
|
|
return sudo !== false ? $`sudo systemctl enable --now ${name}` : $`systemctl enable --now ${name}`
|
|
|
|
} else if (brewInstalled) {
|
|
|
|
return sudo === true ? $`sudo brew services start ${name}` : $`brew services start ${name}`
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
await Promise.allSettled(servicePromises)
|
|
|
|
log(`Installation process complete!`)
|
2024-01-13 21:48:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
main()
|