internal/pkg: write per-artifact logs
All checks were successful
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m36s
Test / ShareFS (push) Successful in 3m42s
Test / Sandbox (race detector) (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 5m50s
Test / Flake checks (push) Successful in 1m28s

This is currently only used by execArtifact. A later patch will add additional logging facilities.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-03-03 22:47:18 +09:00
parent ea87664a75
commit 94e3debc63
3 changed files with 116 additions and 6 deletions

View File

@@ -418,6 +418,12 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
z.Hostname = "cure-net"
}
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
var status io.Writer
if status, err = f.GetStatusWriter(); err != nil {
return
}
if msg := f.GetMessage(); msg.IsVerbose() {
var stdout, stderr io.ReadCloser
if stdout, err = z.StdoutPipe(); err != nil {
@@ -434,10 +440,29 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
}
}()
bw := f.cache.getWriter(status)
defer func() {
flushErr := bw.Flush()
if err == nil {
err = flushErr
}
f.cache.putWriter(bw)
}()
stdoutDone, stderrDone := make(chan struct{}), make(chan struct{})
go scanVerbose(msg, cancel, stdoutDone, "("+a.name+":1)", stdout)
go scanVerbose(msg, cancel, stderrDone, "("+a.name+":2)", stderr)
go scanVerbose(
msg, cancel, stdoutDone,
"("+a.name+":1)",
io.TeeReader(stdout, bw),
)
go scanVerbose(
msg, cancel, stderrDone,
"("+a.name+":2)",
io.TeeReader(stderr, bw),
)
defer func() { <-stdoutDone; <-stderrDone }()
} else {
z.Stdout, z.Stderr = status, status
}
z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args

View File

@@ -28,15 +28,21 @@ import (
"unsafe"
"hakurei.app/container/check"
"hakurei.app/internal/info"
"hakurei.app/internal/lockedfile"
"hakurei.app/message"
)
const (
// programName is the string identifying this build system.
programName = "internal/pkg"
)
type (
// A Checksum is a SHA-384 checksum computed for a cured [Artifact].
Checksum = [sha512.Size384]byte
// An ID is a unique identifier returned by [Artifact.ID]. This value must
// An ID is a unique identifier returned by [KnownIdent.ID]. This value must
// be deterministically determined ahead of time.
ID Checksum
)
@@ -81,20 +87,75 @@ type TContext struct {
// Populated during [Cache.Cure].
work, temp *check.Absolute
// Target [Artifact] encoded identifier.
ids string
// Pathname status was created at.
statusPath *check.Absolute
// File statusHeader and logs are written to.
status *os.File
// Error value during prepareStatus.
statusErr error
common
}
// statusHeader is the header written to all status files in dirStatus.
var statusHeader = func() string {
s := programName
if v := info.Version(); v != info.FallbackVersion {
s += " " + v
}
s += " (" + runtime.GOARCH + ")"
if name, err := os.Hostname(); err == nil {
s += " on " + name
}
s += "\n\n"
return s
}()
// prepareStatus initialises the status file once.
func (t *TContext) prepareStatus() error {
if t.statusPath != nil || t.status != nil {
return t.statusErr
}
t.statusPath = t.cache.base.Append(
dirStatus,
t.ids,
)
if t.status, t.statusErr = os.OpenFile(
t.statusPath.String(),
syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY,
0400,
); t.statusErr != nil {
return t.statusErr
}
_, t.statusErr = t.status.WriteString(statusHeader)
return t.statusErr
}
// GetStatusWriter returns a [io.Writer] for build logs. The caller must not
// seek this writer before the position it was first returned in.
func (t *TContext) GetStatusWriter() (io.Writer, error) {
err := t.prepareStatus()
return t.status, err
}
// destroy destroys the temporary directory and joins its errors with the error
// referred to by errP. If the error referred to by errP is non-nil, the work
// directory is removed similarly. [Cache] is responsible for making sure work
// is never left behind for a successful [Cache.Cure].
//
// If implementation had requested status, it is closed with error joined with
// the error referred to by errP. If the error referred to by errP is non-nil,
// the status file is removed from the filesystem.
//
// destroy must be deferred by [Cache.Cure] if [TContext] is passed to any Cure
// implementation. It should not be called prior to that point.
func (t *TContext) destroy(errP *error) {
if chmodErr, removeErr := removeAll(t.temp); chmodErr != nil || removeErr != nil {
*errP = errors.Join(*errP, chmodErr, removeErr)
return
}
if *errP != nil {
@@ -106,6 +167,16 @@ func (t *TContext) destroy(errP *error) {
*errP = nil
}
}
if t.status != nil {
if err := t.status.Close(); err != nil {
*errP = errors.Join(*errP, err)
}
if *errP != nil {
*errP = errors.Join(*errP, os.Remove(t.statusPath.String()))
}
t.status = nil
}
}
// Unwrap returns the underlying [context.Context].
@@ -169,7 +240,7 @@ type FContext struct {
}
// InvalidLookupError is the identifier of non-dependency [Artifact] looked up
// via [FContext.Pathname] by a misbehaving [Artifact] implementation.
// via [FContext.GetArtifact] by a misbehaving [Artifact] implementation.
type InvalidLookupError ID
func (e InvalidLookupError) Error() string {
@@ -375,6 +446,9 @@ const (
// dirChecksum is the directory name appended to Cache.base for storing
// artifacts named after their [Checksum].
dirChecksum = "checksum"
// dirStatus is the directory name appended to Cache.base for storing
// artifact metadata and logs named after their [ID].
dirStatus = "status"
// dirWork is the directory name appended to Cache.base for working
// pathnames set up during [Cache.Cure].
@@ -943,8 +1017,9 @@ func (c *Cache) openFile(f FileArtifact) (r io.ReadCloser, err error) {
if !errors.Is(err, os.ErrNotExist) {
return
}
id := c.Ident(f)
if c.msg.IsVerbose() {
rn := reportName(f, c.Ident(f))
rn := reportName(f, id)
c.msg.Verbosef("curing %s in memory...", rn)
defer func() {
if err == nil {
@@ -1527,6 +1602,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
t := TContext{
c.base.Append(dirWork, ids),
c.base.Append(dirTemp, ids),
ids, nil, nil, nil,
common{c},
}
switch ca := a.(type) {
@@ -1714,6 +1790,7 @@ func open(
for _, name := range []string{
dirIdentifier,
dirChecksum,
dirStatus,
dirWork,
} {
if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil &&

View File

@@ -314,6 +314,11 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
t.Fatal(err)
}
// destroy non-deterministic status files
if err := os.RemoveAll(base.Append("status").String()); err != nil {
t.Fatal(err)
}
var checksum pkg.Checksum
if err := pkg.HashDir(&checksum, base); err != nil {
t.Fatalf("HashDir: error = %v", err)
@@ -382,6 +387,9 @@ func cureMany(t *testing.T, c *pkg.Cache, steps []cureStep) {
} else if step.pathname != ignorePathname && !pathname.Is(step.pathname) {
t.Fatalf("Cure: pathname = %q, want %q", pathname, step.pathname)
} else if checksum != makeChecksumH(step.checksum) {
if checksum == (unique.Handle[pkg.Checksum]{}) {
checksum = unique.Make(pkg.Checksum{})
}
t.Fatalf(
"Cure: checksum = %s, want %s",
pkg.Encode(checksum.Value()), pkg.Encode(step.checksum),