sigil/overlay/eris-patch-hook/eris_patch.nim

267 lines
9.6 KiB
Nim

import std/[asyncdispatch, deques, json, os, osproc, streams, strutils, tables]
import cbor, dhall/[render, terms], eris
const selfDescribedCbor = 55799
proc writeErisLinks(path: string; node: CborNode) =
# Inspired by "Package Metadata for Core Files"
# - https://systemd.io/COREDUMP_PACKAGE_METADATA/
# - http://netbsd.org/docs/kernel/elf-notes.html
const name = "Sigil\x00"
var links = newStringStream()
links.writeCborTag(selfDescribedCbor)
links.writeCbor(node)
var file = openFileStream(path, fmWrite)
file.write(uint32 name.len)
file.write(uint32 links.data.len)
file.write(uint32 1)
file.write(name)
while (file.getPosition and 3) != 0: file.write(byte 0)
file.write(links.data)
while (file.getPosition and 3) != 0: file.write(byte 0)
close(file)
proc toCbor(cap: ErisCap): CborNode =
result = initCborBytes(cap.bytes)
result.tag = erisCborTag
proc toDhall(js: JsonNode): Value =
case js.kind
of JString:
result = newValue(js.str)
of JObject:
result = newRecordLiteral(js.fields.len)
for key, val in js.fields.pairs:
result.table[key] = val.toDhall
else:
raiseAssert "unhandled JSON value"
proc isElf(path: string): bool =
var magic: array[4, char]
let file = open(path)
discard readChars(file, magic)
close(file)
magic == [0x7f.char, 'E', 'L', 'F']
proc main =
var generateManifests, generateNotes, addNotes, runPatchelf: bool
for arg in commandLineParams():
case arg
of "--add-notes": addNotes = true
of "--run-patchelf": runPatchelf = true
of "--generate-manifests": generateManifests = true
of "--generate-notes": generateNotes = true
if getEnv("dontErisPatch") != "": quit 0
let
patchelf = getEnv("ERIS_PATCHELF", "patchelf")
objcopy = getEnv("ERIS_OBJCOPY", "objcopy")
nixStore = getEnv("NIX_STORE", "/nix/store")
jsonManifestSubPath = "nix-support" / "eris-manifest.json"
dhallManifestSubPath = "nix-support" / "eris-manifest.dhall"
type PendingFile = ref object
outputRoot, filePath: string
replacements: Table[string, string]
erisLinks: CborNode
var
outputManifests = initTable[string, JsonNode]()
pendingFiles = initDeque[PendingFile]()
failed = false
if getEnv("outputs") == "":
quit """package $outputs not set in environment"""
for outputName in getEnv("outputs").splitWhitespace:
if getEnv(outputName) == "":
quit("package $" & outputName & " not set in environment")
let outputRoot = getEnv(outputName)
if fileExists(outputRoot / jsonManifestSubPath):
stderr.writeLine "Not running ERIS patch hook again"
quit 0
outputManifests[outputRoot] = newJObject()
let buildInputs = getEnv("buildInputs").splitWhitespace
proc resolveNeed(rpath: seq[string]; need: string): string =
if need.isAbsolute:
return need
for libdir in rpath:
let absNeed = libdir / need
if fileExists(absNeed):
return absNeed
for outputRoot in outputManifests.keys:
for relPath in [need, "lib" / need]:
let absNeed = outputRoot / relPath
if fileExists(absNeed):
return absNeed
for buildInput in buildInputs:
for relPath in [need, "lib" / need]:
let absNeed = buildInput / relPath
if fileExists(absNeed):
return absNeed
proc resolveFile(outputRoot, filePath: string): PendingFile =
result = PendingFile(
outputRoot: outputRoot,
filePath: filePath,
replacements: initTable[string, string](8),
erisLinks: initCborMap())
let needs = splitWhitespace(execProcess(
patchelf, args = ["--print-needed", filePath], options = {poUsePath}))
let rpath = splitWhitespace(execProcess(
patchelf, args = ["--print-rpath", filePath], options = {poUsePath}))
for need in needs:
if need == "ld.lib.so" or need.startsWith("urn:"): continue
result.replacements[need] = resolveNeed(rpath, need)
var capCache = initTable[string, ErisCap]()
proc fileCap(filePath: string): ErisCap =
## Determine the ERIS read capabililty for ``filePath``.
if capCache.hasKey(filePath):
result = capCache[filePath]
else:
try:
let str = openFileStream(filePath)
result = waitFor encode(newDiscardStore(), str, convergentMode)
capCache["filePath"] = result
close(str)
except:
stderr.writeLine("failed to read \"", filePath, "\"")
quit 1
var closureCache = initTable[string, TableRef[string, ErisCap]]()
proc fileClosure(filePath: string): TableRef[string, ErisCap] =
## Recusively find the dependency closure of ``filePath``.
let filePath = expandFilename filePath
if closureCache.hasKey(filePath):
result = closureCache[filePath]
else:
result = newTable[string, ErisCap]()
var storePath = filePath
for p in parentDirs(filePath):
# find the top directory of the ``filePath`` derivation
if p == nixStore: break
storePath = p
if storePath.startsWith nixStore:
# read the closure manifest of the dependency
let manifestPath = storePath / jsonManifestSubPath
if fileExists(manifestPath):
let
manifest = parseFile(manifestPath)
entry = manifest[filePath.extractFilename]
for path, urn in entry["closure"].pairs:
result[path] = parseErisUrn urn.getStr
let otherClosure = fileClosure(path)
for otherPath, otherCap in otherClosure.pairs:
# merge the closure of the dependency
result[otherPath] = otherCap
closureCache[filePath] = result
for outputRoot in outputManifests.keys:
let manifestPath = outputRoot / jsonManifestSubPath
if fileExists manifestPath: continue
for filePath in walkDirRec(outputRoot, relative = false):
# Populate the queue of files to patch
if filePath.isElf:
pendingFiles.addLast(resolveFile(outputRoot, filePath))
var
prevLen = pendingFiles.len
prevPrevLen = prevLen.succ
# used to detect reference cycles
while pendingFiles.len != 0:
block selfReferenceCheck:
# process the files that have been collected
# taking care not to take a the URN of an
# unprocessed file
let
pendingFile = pendingFiles.popFirst()
filePath = pendingFile.filePath
for need, replacementPath in pendingFile.replacements.pairs:
# search for self-references
if replacementPath == "":
stderr.writeLine need, " not found for ", filePath
failed = true
continue
for outputRoot in outputManifests.keys:
if replacementPath.startsWith(outputRoot):
for other in pendingFiles.items:
stderr.writeLine "compare for self-reference:"
stderr.writeLine '\t', replacementPath
stderr.writeLine '\t', other.filePath
if replacementPath == other.filePath:
stderr.writeLine "defering patch of ", filePath, " with reference to ", other.filePath
pendingFiles.addLast(pendingFile)
break selfReferenceCheck
var
closure = newJObject()
replaceCmd = patchelf & " --set-rpath '' " & filePath
for need, replacementPath in pendingFile.replacements.pairs:
if replacementPath == "": continue
let
cap = fileCap(replacementPath)
urn = $cap
stderr.writeLine "replace reference to ", need, " with ", urn
closure[replacementPath] = %urn
pendingFile.erisLinks[ replacementPath.toCbor] = cap.toCbor
replaceCmd.add(" --replace-needed $# $#" % [need, urn])
for path, cap in fileClosure(replacementPath).pairs:
closure[path] = %($cap)
pendingFile.erisLinks[path.toCbor] = cap.toCbor
if runPatchelf and pendingFile.replacements.len != 0:
let exitCode = execCmd(replaceCmd)
if exitCode != 0:
stderr.writeLine "Patchelf failed - ", replaceCmd
quit exitCode
let notePath = getEnv("TMPDIR", ".") / "eris-links.note"
if generateNotes or addNotes:
if generateNotes:
stderr.writeLine("writing .notes.eris-links section to ", notePath)
sort(pendingFile.erisLinks)
writeErisLinks(notePath, pendingFile.erisLinks)
if addNotes:
# Modify the file with objcopy last because binutils surely
# has a more conventional interpretation of the ELF standard
# than patchelf.
let tmpFile = pendingFile.filePath & ".with-note"
let objcopyCommand = [
objcopy,
"--add-section .note.eris-links=" & notePath,
"--set-section-flags .note.eris-links=noload,readonly",
pendingFile.filePath, tmpfile]
let exitCode = execCmd(objcopyCommand.join(" "))
if exitCode != 0:
quit("Adding note to $1 failed" % pendingFile.filePath)
moveFile(tmpFile, pendingFile.filePath)
outputManifests[pendingFile.outputRoot][filePath.extractFilename] = %* {
"cap": $fileCap(filePath),
"closure": closure,
"path": filePath
}
if pendingFiles.len == prevPrevLen:
failed = true
stderr.writeLine "reference cycle detected in the following:"
for remain in pendingFiles.items:
stderr.writeLine '\t', " ", remain.filePath
break
prevPrevLen = prevLen
prevLen = pendingFiles.len
if failed:
quit -1
if generateManifests:
for outputRoot, manifest in outputManifests:
let supportDir = outputRoot / "nix-support"
createDir(supportDir)
writeFile(outputRoot / jsonManifestSubPath, $manifest)
writeFile(outputRoot / dhallManifestSubPath, $(manifest.toDhall))
main()