mirror of
https://github.com/ivabus/pantry
synced 2024-11-10 18:45:19 +03:00
291 lines
9.4 KiB
TypeScript
291 lines
9.4 KiB
TypeScript
|
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 wouldn’t 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 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')
|
|||
|
}
|
|||
|
}
|