mirror of
https://github.com/ivabus/pantry
synced 2024-11-23 08:55:07 +03:00
172 lines
5.5 KiB
TypeScript
Executable file
172 lines
5.5 KiB
TypeScript
Executable file
#!/usr/bin/env -S tea -E
|
||
|
||
/*---
|
||
args:
|
||
- deno
|
||
- run
|
||
- --allow-run
|
||
- --allow-env
|
||
- --allow-read
|
||
- --allow-write={{tea.prefix}}
|
||
- --import-map={{ srcroot }}/import-map.json
|
||
dependencies:
|
||
nixos.org/patchelf: '*'
|
||
darwinsys.com/file: 5
|
||
---*/
|
||
|
||
import { useCellar } from "hooks"
|
||
import { PackageRequirement, Installation, Package } from "types"
|
||
import { backticks, run, host, pkg as pkgutils } from "utils"
|
||
import Path from "path"
|
||
|
||
|
||
if (import.meta.main) {
|
||
const cellar = useCellar()
|
||
const [installation, ...pkgs] = Deno.args
|
||
await fix_rpaths(
|
||
await cellar.resolve(new Path(installation)),
|
||
pkgs.map(pkgutils.parse)
|
||
)
|
||
}
|
||
|
||
|
||
//TODO this is not resilient to upgrades (obv)
|
||
//NOTE solution is to have the rpath reference major version (or more specific if poss)
|
||
|
||
/// fix rpaths or install names for executables and dynamic libraries
|
||
export default async function fix_rpaths(installation: Installation, pkgs: (Package | PackageRequirement)[]) {
|
||
const skip_rpaths = [
|
||
"go.dev", // skipping because for some reason patchelf breaks the go binary resulting in the only output being: `Segmentation Fault`
|
||
"tea.xyz", // this causes tea to pass -E/--version (and everything else?) directly to deno, making it _too_ much of a wrapper.
|
||
]
|
||
if (skip_rpaths.includes(installation.pkg.project)) {
|
||
console.info(`skipping rpath fixes for ${installation.pkg.project}`)
|
||
return
|
||
}
|
||
console.info("doing SLOW rpath fixes…")
|
||
for await (const [exename] of exefiles(installation.path)) {
|
||
await set_rpaths(exename, pkgs, installation)
|
||
}
|
||
}
|
||
|
||
|
||
//TODO it's an error if any binary has bad rpaths before bottling
|
||
//NOTE we should have a `safety-inspector` step before bottling to check for this sort of thing
|
||
// and then have virtual env manager be more specific via (DY)?LD_LIBRARY_PATH
|
||
//FIXME somewhat inefficient for eg. git since git is mostly hardlinks to the same file
|
||
async function set_rpaths(exename: Path, pkgs: (Package | PackageRequirement)[], installation: Installation) {
|
||
if (host().platform != 'linux') throw new Error()
|
||
|
||
const cellar = useCellar()
|
||
const our_rpaths = await Promise.all(pkgs.map(pkg => prefix(pkg)))
|
||
|
||
const cmd = await (async () => {
|
||
//FIXME we need this for perl
|
||
// however really we should just have an escape hatch *just* for stuff that sets its own rpaths
|
||
const their_rpaths = (await backticks({
|
||
cmd: ["patchelf", "--print-rpath", exename],
|
||
}))
|
||
.split(":")
|
||
.compact(x => x.chuzzle())
|
||
//^^ split has ridiculous empty string behavior
|
||
|
||
const rpaths = [...their_rpaths, ...our_rpaths]
|
||
.map(x => {
|
||
const transformed = transform(x, installation)
|
||
if (transformed.startsWith("$ORIGIN")) {
|
||
console.warn("has own special rpath", transformed)
|
||
return transformed
|
||
} else {
|
||
const rel_path = new Path(transformed).relative({ to: exename.parent() })
|
||
return `$ORIGIN/${rel_path}`
|
||
}
|
||
})
|
||
.uniq()
|
||
.join(':')
|
||
?? []
|
||
|
||
//FIXME use runtime-path since then LD_LIBRARY_PATH takes precedence which our virtual env manager requires
|
||
return ["patchelf", "--force-rpath", "--set-rpath", rpaths, exename]
|
||
})()
|
||
|
||
if (cmd.length) {
|
||
try {
|
||
await run({ cmd })
|
||
} catch (err) {
|
||
console.warn(err)
|
||
//FIXME allowing this error because on Linux:
|
||
// patchelf: cannot find section '.dynamic'. The input file is most likely statically linked
|
||
// happens with eg. gofmt
|
||
// and we don't yet have a good way to detect and skip such files
|
||
}
|
||
}
|
||
|
||
async function prefix(pkg: Package | PackageRequirement) {
|
||
return (await cellar.resolve(pkg)).path.join("lib").string
|
||
}
|
||
}
|
||
|
||
//FIXME pretty slow since we execute `file` for every file
|
||
// eg. perl has hundreds of `.pm` files in its `lib`
|
||
async function* exefiles(prefix: Path): AsyncGenerator<[Path, 'exe' | 'lib']> {
|
||
for (const basename of ["bin", "lib", "libexec"]) {
|
||
const d = prefix.join(basename).isDirectory()
|
||
if (!d) continue
|
||
for await (const [exename, { isFile, isSymlink }] of d.walk()) {
|
||
if (!isFile || isSymlink) continue
|
||
const type = await exetype(exename)
|
||
if (type) yield [exename, type]
|
||
}
|
||
}
|
||
}
|
||
|
||
//FIXME lol use https://github.com/sindresorhus/file-type when we can
|
||
export async function exetype(path: Path): Promise<'exe' | 'lib' | false> {
|
||
// speed this up a bit
|
||
switch (path.extname()) {
|
||
case ".py":
|
||
case ".pyc":
|
||
case ".pl":
|
||
return false
|
||
}
|
||
|
||
const out = await backticks({
|
||
cmd: ["file", "--mime-type", path.string]
|
||
})
|
||
const lines = out.split("\n")
|
||
const line1 = lines[0]
|
||
if (!line1) throw new Error()
|
||
const match = line1.match(/: (.*)$/)
|
||
if (!match) throw new Error()
|
||
const mime = match[1]
|
||
|
||
console.debug(mime)
|
||
|
||
switch (mime) {
|
||
case 'application/x-pie-executable':
|
||
case 'application/x-mach-binary':
|
||
case 'application/x-executable':
|
||
return 'exe'
|
||
|
||
case 'application/x-sharedlib':
|
||
return 'lib'
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// convert a full version path to a major’d version path
|
||
// this so we are resilient to upgrades without requiring us to rewrite binaries on install
|
||
// since rewriting binaries would invalidate our signatures
|
||
function transform(input: string, installation: Installation) {
|
||
if (input.startsWith("$ORIGIN")) {
|
||
// we leave these alone, trusting the build tool knew what it was doing
|
||
return input
|
||
} else if (input.startsWith(installation.path.parent().string)) {
|
||
// don’t transform stuff that links to this actual package
|
||
return input
|
||
} else {
|
||
//FIXME not very robust lol
|
||
return input.replace(/v(\d+)\.\d+\.\d+/, 'v$1')
|
||
}
|
||
}
|