'use strict'; var InstallerServer = module.exports; let Fs = require('fs/promises'); let Path = require('path'); let HostTargets = require('./build-classifier/host-targets.js'); let Builds = require('./builds.js'); let Installers = require('./installers.js'); InstallerServer.INSTALLERS_DIR = Path.join(__dirname, '..'); InstallerServer.serveInstaller = async function ( baseurl, ua, pkg, tag, ext, formats, libc, ) { let unameAgent = ua; let projectName = pkg; let [rel, tmplParams] = await InstallerServer.helper({ unameAgent, projectName, tag, formats, libc, }); Object.assign(tmplParams, { baseurl, }); var pkgdir = Path.join(InstallerServer.INSTALLERS_DIR, projectName); if ('ps1' === ext) { return Installers.renderPowerShell(pkgdir, rel, tmplParams); } return Installers.renderBash(pkgdir, rel, tmplParams); }; // TODO put some of this in a middleware? or common function? // TODO maybe move package/version/lts/channel detection into getReleases InstallerServer.helper = async function ({ unameAgent, projectName, tag, formats, libc, }) { console.log(`dbg: Installer User-Agent: ${unameAgent}`); let releaseTarget = toReleaseTarget(tag); let hostFormats = formats; let terms = unameAgent.split(/[\s\/]+/g); let hostTarget = {}; try { void HostTargets.termsToTarget(hostTarget, terms); } catch (e) { // if we can't guarantee the results... // "in the face of ambiguity, refuse the temptation to guess" throw e; } console.log(`dbg: Installer Host Target:`); console.log(hostTarget); if (!hostTarget.os) { throw new Error(`OS could not be identified by User-Agent '${unameAgent}'`); } console.log(`dbg: Get Project Installer Type for '${projectName}':`); let proj = await Builds.getProjectType(projectName); if (proj.type === 'alias') { console.log(`dbg: alias`, proj); projectName = proj.detail; proj = await Builds.getProjectType(projectName); // an alias should never resolve to an alias } console.log(`dbg: proj`, proj); let validTypes = ['selfhosted', 'valid']; if (!validTypes.includes(proj.type)) { let msg = `'${projectName}' doesn't have an installer: '${proj.type}': '${proj.detail}'`; let err = new Error(msg); err.code = 'ENOENT'; throw err; } let tmplParams = { pkg: projectName, tag: tag, os: hostTarget.os, arch: hostTarget.arch, libc: hostTarget.libc, formats: hostFormats, limit: 1, }; Object.assign(tmplParams, releaseTarget); console.log('tmplParams', tmplParams); let errPackage = { name: 'doesntexist.ext', version: '0.0.0', lts: '-', channel: 'error', date: '1970-01-01', os: hostTarget.os || '-', arch: hostTarget.arch || '-', libc: hostTarget.libc || '-', ext: 'err', download: 'https://example.com/doesntexist.ext', comment: 'No matches found. Could be bad or missing version info' + ',' + "Check query parameters. Should be something like '/api/releases/{package}@{version}.tab?os={macos|linux|windows|-}&arch={amd64|x86|aarch64|arm64|armv7l|-}&libc={musl|gnu|msvc|libc|static}&limit=10'", }; if (proj.type === 'selfhosted') { return [errPackage, tmplParams]; } let projInfo = await Builds.getPackage({ name: projectName, date: new Date(), }); let latestVersions = Builds.enumerateLatestVersions(projInfo); //console.log('projInfo', projInfo); let buildTargetInfo = { triplets: projInfo.triplets, oses: projInfo.oses, arches: projInfo.arches, libcs: projInfo.libcs, formats: projInfo.formats, latest: latestVersions.latest, stable: latestVersions.stable, }; // TODO .findMatchingPackages() should probably account for this let hasOs = projInfo.oses.includes(hostTarget.os); let maybePosix = !hasOs && hostTarget.os !== 'windows'; if (maybePosix) { let posixes = ['posix_2017', 'posix_2024']; for (let posixYear of posixes) { let hasPosix = projInfo.oses.includes(posixYear); if (hasPosix) { hasOs = true; break; } } } if (!hasOs) { hasOs = projInfo.oses.includes('ANYOS'); } if (!hasOs) { let pkg1 = Object.assign(buildTargetInfo, errPackage); return [pkg1, tmplParams]; } let targetRelease = Builds.findMatchingPackages( projInfo, hostTarget, releaseTarget, ); // { triplet: `${os}-${arch}-${libc}`, packages: targetPackages // , latest: projInfo.versions[0], versions: matchInfo // } if (!targetRelease?.packages) { let pkg1 = Object.assign(buildTargetInfo, errPackage); return [pkg1, tmplParams]; } let buildPkg = Builds.selectPackage(targetRelease.packages, hostFormats); let ext = buildPkg.ext || '.exe'; if (ext.startsWith('.')) { ext = ext.slice(1); } let version = targetRelease.version; if (version.startsWith('v')) { version = version.slice(1); } buildPkg = Object.assign(buildTargetInfo, buildPkg, { ext, version }); console.log('dbg: buildPkg', buildPkg); console.log('dbg: tmplParams', tmplParams); return [buildPkg, tmplParams]; }; let channelNames = [ 'stable', // 'hotfix', 'latest', 'rc', 'preview', 'pre', 'dev', 'beta', 'alpha', ]; function toReleaseTarget(tag) { tag = tag.replace(/^v/, ''); let releaseTarget = { channel: '', lts: false, version: '', }; if (tag === 'lts') { releaseTarget.lts = true; releaseTarget.channel = 'stable'; } else if (channelNames.includes(tag)) { releaseTarget.channel = tag; } else { releaseTarget.version = tag; } return releaseTarget; } var CURL_PIPE_PS1_BOOT = Path.join(__dirname, 'curl-pipe-bootstrap.tpl.ps1'); var CURL_PIPE_SH_BOOT = Path.join(__dirname, 'curl-pipe-bootstrap.tpl.sh'); var BAD_SH_RE = /[<>'"`$\\]/; InstallerServer.getPosixCurlPipeBootstrap = async function ({ baseurl, pkg, ver, }) { let bootTxt = await Fs.readFile(CURL_PIPE_SH_BOOT, 'utf8'); var webiPkg = [pkg, ver].filter(Boolean).join('@'); var webiChecksum = await Installers.getWebiShChecksum(); var envReplacements = [ ['WEBI_PKG', webiPkg], ['WEBI_HOST', baseurl], ['WEBI_CHECKSUM', webiChecksum], ]; for (let env of envReplacements) { let name = env[0]; let value = env[1]; let envRe = new RegExp( `^[ \\t]*#?[ \\t]*(export[ \\t])?[ \\t]*(${name})=.*`, 'm', ); if (BAD_SH_RE.test(value)) { throw new Error(`key '${name}' has invalid value '${value}'`); } bootTxt = bootTxt.replace(envRe, `$1$2='${value}'`); } // TODO init config here //bootTxt.replace(/CHEATSHEET_URL/g, `${Config.cheatUrl}/${pkg}`); return bootTxt; }; InstallerServer.getPwshCurlPipeBootstrap = async function ({ baseurl, pkg, ver, exename, }) { let bootTxt = await Fs.readFile(CURL_PIPE_PS1_BOOT, 'utf8'); var webiPkg = [pkg, ver].filter(Boolean).join('@'); //var webiChecksum = await InstallerServer.getWebiPs1Checksum(); var envReplacements = [ ['Env:WEBI_PKG', webiPkg], ['Env:WEBI_HOST', baseurl], //['Env:WEBI_CHECKSUM', webiChecksum], ['baseurl', baseurl], ['exename', exename], ['version', ver], ]; for (let env of envReplacements) { let name = env[0]; let value = env[1]; if (BAD_SH_RE.test(value)) { throw new Error(`key '${name}' has invalid value '${value}'`); } let tplRe = new RegExp(`{{ (${name}) }}`, 'g'); bootTxt = bootTxt.replace(tplRe, `${value}`); let setRe = new RegExp( `(#[ \\t]*)?(\\$${name})[ \\t]*=[ \\t]['"].*['"][ \\t]`, 'im', ); bootTxt = bootTxt.replace(setRe, `$$${name} = '${value}'`); } // TODO init config here //bootTxt.replace(/CHEATSHEET_URL/g, `${Config.cheatUrl}/${pkg}`); return bootTxt; };