From 06dbdf7b2675253350f2ff5bce0e4613e4256bdb Mon Sep 17 00:00:00 2001 From: Ophestra Date: Tue, 3 Mar 2026 22:47:18 +0900 Subject: [PATCH] internal/pkg: write per-artifact logs This is currently only used by execArtifact. A later patch will add additional logging facilities. Signed-off-by: Ophestra --- internal/pkg/exec.go | 29 +++++++++++- internal/pkg/pkg.go | 97 +++++++++++++++++++++++++++++++++++++--- internal/pkg/pkg_test.go | 8 ++++ 3 files changed, 126 insertions(+), 8 deletions(-) diff --git a/internal/pkg/exec.go b/internal/pkg/exec.go index bc068d1..e06ec32 100644 --- a/internal/pkg/exec.go +++ b/internal/pkg/exec.go @@ -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 diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go index 5f6f15d..373a02c 100644 --- a/internal/pkg/pkg.go +++ b/internal/pkg/pkg.go @@ -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 ) @@ -70,6 +76,80 @@ type common struct { // Address of underlying [Cache], should be zeroed or made unusable after // Cure returns and must not be exposed directly. cache *Cache + + // 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 +} + +// 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 (c *common) prepareStatus() error { + if c.statusPath != nil || c.status != nil { + return c.statusErr + } + + c.statusPath = c.cache.base.Append( + dirStatus, + c.ids, + ) + if c.status, c.statusErr = os.OpenFile( + c.statusPath.String(), + syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, + 0400, + ); c.statusErr != nil { + return c.statusErr + } + + if _, c.statusErr = c.status.WriteString(statusHeader); c.statusErr != nil { + c.close(&c.statusErr) + } + return c.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 (c *common) GetStatusWriter() (io.Writer, error) { + err := c.prepareStatus() + return c.status, err +} + +// close closes status and joins its errors 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. +// +// close must be deferred by [Cache.Cure] if common is passed to any Cure +// implementation. +func (c *common) close(errP *error) { + if c.status == nil { + return + } + + if err := c.status.Close(); err != nil { + *errP = errors.Join(*errP, err) + } + if *errP != nil { + *errP = errors.Join(*errP, os.Remove(c.statusPath.String())) + } + c.status = nil } // TContext is passed to [TrivialArtifact.Cure] and provides information and @@ -169,7 +249,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 +455,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 +1026,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 { @@ -952,7 +1036,7 @@ func (c *Cache) openFile(f FileArtifact) (r io.ReadCloser, err error) { } }() } - return f.Cure(&RContext{common{c}}) + return f.Cure(&RContext{common{cache: c, ids: Encode(id.Value())}}) } return } @@ -1448,7 +1532,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) ( if err = c.enterCure(a, curesExempt); err != nil { return } - r, err = f.Cure(&RContext{common{c}}) + r, err = f.Cure(&RContext{common{cache: c, ids: ids}}) if err == nil { if checksumPathname == nil || c.IsStrict() { h := sha512.New384() @@ -1527,7 +1611,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) ( t := TContext{ c.base.Append(dirWork, ids), c.base.Append(dirTemp, ids), - common{c}, + common{cache: c, ids: ids}, } switch ca := a.(type) { case TrivialArtifact: @@ -1714,6 +1798,7 @@ func open( for _, name := range []string{ dirIdentifier, dirChecksum, + dirStatus, dirWork, } { if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil && diff --git a/internal/pkg/pkg_test.go b/internal/pkg/pkg_test.go index 700aa96..f8a7256 100644 --- a/internal/pkg/pkg_test.go +++ b/internal/pkg/pkg_test.go @@ -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),