pantry/scripts/fix-rpaths.ts
2022-09-16 15:35:46 -04:00

291 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import useCellar from "hooks/useCellar.ts"
import usePlatform from "hooks/usePlatform.ts"
import { Path, PackageRequirement, Installation } from "types"
import { runAndGetOutput,run } from "utils"
if (import.meta.main) {
console.log(await get_rpaths(new Path(Deno.args[0])))
}
//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: PackageRequirement[]) {
if (installation.pkg.project == "go.dev") {
console.info("skipping rpath fixes for go.dev")
// skipping because for some reason patchelf breaks the go binary
// resulting in the only output being: `Segmentation Fault`
return
}
console.info("doing SLOW rpath fixes…")
for await (const [exename, type] of exefiles(installation.path)) {
await set_rpaths(exename, type, pkgs, installation)
}
}
const platform = usePlatform().platform
//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, type: 'exe' | 'lib', pkgs: PackageRequirement[], installation: Installation) {
const cellar = useCellar()
const our_rpaths = await Promise.all(pkgs.map(pkg => prefix(pkg)))
const cmd = await (async () => {
switch (platform) {
case 'linux': {
//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 runAndGetOutput({
cmd: ["patchelf", "--print-rpath", exename],
}))
.split(":")
.compactMap(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]
}
case 'darwin': {
const rpath = cellar.prefix.relative({ to: exename.parent() })
const args: (string | Path)[] = [
"install_name_tool"
]
// both types need rpath set or things linking to eg. independent libs
// will fail to find the transitive shit, especially in configure scripts
if (type == 'lib') {
// we can't trust the id the build system picked
// we need dependents to correctly link to this dylib
// and they often use the `id` to do so
// we tried setting it to @rpath/project/dir/lib but that was probematic since linked executables wouldnt find the libs at *runtime*
//TODO possibly should transform to the major of this…
args.push(...[
"-id", exename!
])
}
for (const old_path of await get_bad_otool_listings(exename, type) ?? []) {
const dylib = await find_dylib(old_path, installation)
if (!dylib) {
console.error({old_path, installation, exename, type})
throw new Error()
}
//TODO ^^ probs should look through deps too
const new_path = (() => {
if (dylib.string.startsWith(installation.path.string)) {
const relname = dylib.relative({ to: exename.parent() })
return `@loader_path/${relname}`
} else {
const transformed = transform(dylib.string, installation)
const rel_path = new Path(transformed).relative({ to: cellar.prefix })
return `@rpath/${rel_path}`
}
})()
args.push("-change", old_path, new_path)
}
if (args.length == 1) return []
// install_name_tool barfs if the rpath already exists
if (!(await get_rpaths(exename)).includes(rpath)) {
args.push("-add_rpath", `@loader_path/${rpath}`)
}
if (args.length == 1) return []
args.push(exename.string)
return args
}
case 'windows':
throw new Error()
}
})()
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: PackageRequirement) {
return (await cellar.resolve(pkg)).path.join("lib").string
}
}
async function get_rpaths(exename: Path): Promise<string[]> {
//GOOD_1ST_ISSUE better tokenizer for the output
const lines = (await runAndGetOutput({
cmd: ["otool", "-l", exename]
}))
.trim()
.split("\n")
const it = lines.values()
const rv: string[] = []
for (const line of it) {
if (line.trim().match(/^cmd\s+LC_RPATH$/)) {
it.next()
rv.push(it.next().value.trim().match(/^path\s+(.+)$/)[1])
console.debug(rv.slice(-1)[0])
}
}
return rv
}
async function find_dylib(name: string, installation: Installation) {
if (name.startsWith("/")) {
return new Path(name)
} else {
for await (const [path, {name: basename}] of installation.path.join("lib").ls()) {
if (basename == name) return path
}
}
}
async function get_bad_otool_listings(exename: Path, type: 'exe' | 'lib'): Promise<string[]> {
const cellar = useCellar()
const lines = (await runAndGetOutput({cmd: ["otool", "-L", exename]}))
.trim()
.split("\n")
.slice(1)
// ^^ dylibs list themselves on 1st and 2nd lines
// NOTE that if the file is named .so then this is not true even though it is still a dylib
// NOBODY KNOW WHY
const rv: string[] = []
for (const line of lines) {
console.debug(line)
const match = line.match(/\t(.+) \(compatibility version/)
if (!match) throw new Error()
const dylib = match[1]
if (type == 'lib' && dylib.split("/").slice(-1)[0] == exename.basename()) {
// the dylib has an `id`, note that not all dylibs have ids (somehow)
// note the only place we found this true was python
//FIXME the above check is not really sufficient but ids may be expressed as relative or absolute paths
// and their base is not always clear
continue
}
if (dylib.startsWith(cellar.prefix.string)) {
rv.push(dylib)
}
if (dylib.startsWith("@")) {
console.warn("build created its own special dyld entry: " + dylib)
} else if (!dylib.startsWith("/")) {
rv.push(dylib)
}
}
return rv
}
//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 runAndGetOutput({
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':
if (platform != 'darwin') return 'exe'
//FIXME on darwin the `file` utility returns x-mach-binary for both binary types
switch (path.extname()) {
case ".dylib":
case ".so": // lol python has .so files even on macOS
return 'lib'
case ".o":
return false
default:
if (path.parent().components().includes('lib')) return 'lib'
return 'exe'
}
return false
case 'application/x-sharedlib':
return 'lib'
default:
return false
}
}
// convert a full version path to a majord 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)) {
// dont 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')
}
}