aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-04-16 16:00:32 +0900
committerOphestra <cat@gensokyo.uk>2026-04-16 17:43:52 +0900
commit823575acaccfb34d908dad2ae14551fa425d7431 (patch)
treef9df2fc4a28169e89a90a8477188e84bbe3077b2 /cmd
parent136bc0917be7c02b818bfc8d4d54ec8ffbc8d21b (diff)
cmd/mbf: move info command
This is cleaner with less shared state. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mbf/info.go127
-rw-r--r--cmd/mbf/info_test.go170
-rw-r--r--cmd/mbf/main.go95
3 files changed, 298 insertions, 94 deletions
diff --git a/cmd/mbf/info.go b/cmd/mbf/info.go
new file mode 100644
index 00000000..f421ce66
--- /dev/null
+++ b/cmd/mbf/info.go
@@ -0,0 +1,127 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "hakurei.app/internal/pkg"
+ "hakurei.app/internal/rosa"
+)
+
+// commandInfo implements the info subcommand.
+func commandInfo(
+ cm *cache,
+ args []string,
+ w io.Writer,
+ writeStatus bool,
+ reportPath string,
+) (err error) {
+ if len(args) == 0 {
+ return errors.New("info requires at least 1 argument")
+ }
+
+ var r *rosa.Report
+ if reportPath != "" {
+ if r, err = rosa.OpenReport(reportPath); err != nil {
+ return err
+ }
+ defer func() {
+ if closeErr := r.Close(); err == nil {
+ err = closeErr
+ }
+ }()
+ defer r.HandleAccess(&err)()
+ }
+
+ // recovered by HandleAccess
+ mustPrintln := func(a ...any) {
+ if _, _err := fmt.Fprintln(w, a...); _err != nil {
+ panic(_err)
+ }
+ }
+ mustPrint := func(a ...any) {
+ if _, _err := fmt.Fprint(w, a...); _err != nil {
+ panic(_err)
+ }
+ }
+
+ for i, name := range args {
+ if p, ok := rosa.ResolveName(name); !ok {
+ return fmt.Errorf("unknown artifact %q", name)
+ } else {
+ var suffix string
+ if version := rosa.Std.Version(p); version != rosa.Unversioned {
+ suffix += "-" + version
+ }
+ mustPrintln("name : " + name + suffix)
+
+ meta := rosa.GetMetadata(p)
+ mustPrintln("description : " + meta.Description)
+ if meta.Website != "" {
+ mustPrintln("website : " +
+ strings.TrimSuffix(meta.Website, "/"))
+ }
+ if len(meta.Dependencies) > 0 {
+ mustPrint("depends on :")
+ for _, d := range meta.Dependencies {
+ s := rosa.GetMetadata(d).Name
+ if version := rosa.Std.Version(d); version != rosa.Unversioned {
+ s += "-" + version
+ }
+ mustPrint(" " + s)
+ }
+ mustPrintln()
+ }
+
+ const statusPrefix = "status : "
+ if writeStatus {
+ if r == nil {
+ var f io.ReadSeekCloser
+ err = cm.Do(func(cache *pkg.Cache) (err error) {
+ f, err = cache.OpenStatus(rosa.Std.Load(p))
+ return
+ })
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ mustPrintln(
+ statusPrefix + "not yet cured",
+ )
+ } else {
+ return
+ }
+ } else {
+ mustPrint(statusPrefix)
+ _, err = io.Copy(w, f)
+ if err = errors.Join(err, f.Close()); err != nil {
+ return
+ }
+ }
+ } else if err = cm.Do(func(cache *pkg.Cache) (err error) {
+ status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
+ if status == nil {
+ mustPrintln(
+ statusPrefix + "not in report",
+ )
+ } else {
+ mustPrintln("size :", n)
+ mustPrint(statusPrefix)
+ if _, err = w.Write(status); err != nil {
+ return
+ }
+ }
+ return
+ }); err != nil {
+ return
+ }
+ }
+
+ if i != len(args)-1 {
+ mustPrintln()
+ }
+ }
+ }
+ return nil
+}
diff --git a/cmd/mbf/info_test.go b/cmd/mbf/info_test.go
new file mode 100644
index 00000000..16e94364
--- /dev/null
+++ b/cmd/mbf/info_test.go
@@ -0,0 +1,170 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "syscall"
+ "testing"
+ "unsafe"
+
+ "hakurei.app/internal/pkg"
+ "hakurei.app/internal/rosa"
+ "hakurei.app/message"
+)
+
+func TestInfo(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ args []string
+ status map[string]string
+ report string
+ want string
+ wantErr any
+ }{
+ {"qemu", []string{"qemu"}, nil, "", `
+name : qemu-` + rosa.Std.Version(rosa.QEMU) + `
+description : a generic and open source machine emulator and virtualizer
+website : https://www.qemu.org
+depends on : glib-` + rosa.Std.Version(rosa.GLib) + ` zstd-` + rosa.Std.Version(rosa.Zstd) + `
+`, nil},
+
+ {"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
+name : hakurei-` + rosa.Std.Version(rosa.Hakurei) + `
+description : low-level userspace tooling for Rosa OS
+website : https://hakurei.app
+
+name : hakurei-dist-` + rosa.Std.Version(rosa.HakureiDist) + `
+description : low-level userspace tooling for Rosa OS (distribution tarball)
+website : https://hakurei.app
+`, nil},
+
+ {"nonexistent", []string{"zlib", "\x00"}, nil, "", `
+name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
+description : lossless data-compression library
+website : https://zlib.net
+
+`, fmt.Errorf("unknown artifact %q", "\x00")},
+
+ {"status cache", []string{"zlib", "zstd"}, map[string]string{
+ "zstd": "internal/pkg (amd64) on satori\n",
+ "hakurei": "internal/pkg (amd64) on satori\n\n",
+ }, "", `
+name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
+description : lossless data-compression library
+website : https://zlib.net
+status : not yet cured
+
+name : zstd-` + rosa.Std.Version(rosa.Zstd) + `
+description : a fast compression algorithm
+website : https://facebook.github.io/zstd
+status : internal/pkg (amd64) on satori
+`, nil},
+
+ {"status cache perm", []string{"zlib"}, map[string]string{
+ "zlib": "\x00",
+ }, "", `
+name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
+description : lossless data-compression library
+website : https://zlib.net
+`, func(cm *cache) error {
+ return &os.PathError{
+ Op: "open",
+ Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(rosa.Std.Load(rosa.Zlib)).Value())),
+ Err: syscall.EACCES,
+ }
+ }},
+
+ {"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), `
+name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
+description : lossless data-compression library
+website : https://zlib.net
+status : not in report
+`, nil},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ var (
+ cm *cache
+ buf strings.Builder
+ rp string
+ )
+
+ if tc.status != nil || tc.report != "" {
+ cm = &cache{
+ ctx: context.Background(),
+ msg: message.New(log.New(os.Stderr, "info: ", 0)),
+ base: t.TempDir(),
+ }
+ defer cm.Close()
+ }
+
+ if tc.report != "" {
+ rp = filepath.Join(t.TempDir(), "report")
+ if err := os.WriteFile(
+ rp,
+ unsafe.Slice(unsafe.StringData(tc.report), len(tc.report)),
+ 0400,
+ ); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ if tc.status != nil {
+ for name, status := range tc.status {
+ p, ok := rosa.ResolveName(name)
+ if !ok {
+ t.Fatalf("invalid name %q", name)
+ }
+ perm := os.FileMode(0400)
+ if status == "\x00" {
+ perm = 0
+ }
+ if err := cm.Do(func(cache *pkg.Cache) error {
+ return os.WriteFile(filepath.Join(
+ cm.base,
+ "status",
+ pkg.Encode(cache.Ident(rosa.Std.Load(p)).Value()),
+ ), unsafe.Slice(unsafe.StringData(status), len(status)), perm)
+ }); err != nil {
+ t.Fatalf("Do: error = %v", err)
+ }
+ }
+ }
+
+ var wantErr error
+ switch c := tc.wantErr.(type) {
+ case error:
+ wantErr = c
+ case func(cm *cache) error:
+ wantErr = c(cm)
+ default:
+ if tc.wantErr != nil {
+ t.Fatalf("invalid wantErr %#v", tc.wantErr)
+ }
+ }
+
+ if err := commandInfo(
+ cm,
+ tc.args,
+ &buf,
+ cm != nil,
+ rp,
+ ); !reflect.DeepEqual(err, wantErr) {
+ t.Fatalf("commandInfo: error = %v, want %v", err, wantErr)
+ }
+
+ if got := buf.String(); got != strings.TrimPrefix(tc.want, "\n") {
+ t.Errorf("commandInfo:\n%s\nwant\n%s", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/cmd/mbf/main.go b/cmd/mbf/main.go
index bfa2aa07..675f8bb2 100644
--- a/cmd/mbf/main.go
+++ b/cmd/mbf/main.go
@@ -24,7 +24,6 @@ import (
"path/filepath"
"runtime"
"strconv"
- "strings"
"sync"
"sync/atomic"
"syscall"
@@ -149,99 +148,7 @@ func main() {
"info",
"Display out-of-band metadata of an artifact",
func(args []string) (err error) {
- if len(args) == 0 {
- return errors.New("info requires at least 1 argument")
- }
-
- var r *rosa.Report
- if flagReport != "" {
- if r, err = rosa.OpenReport(flagReport); err != nil {
- return err
- }
- defer func() {
- if closeErr := r.Close(); err == nil {
- err = closeErr
- }
- }()
- defer r.HandleAccess(&err)()
- }
-
- for i, name := range args {
- if p, ok := rosa.ResolveName(name); !ok {
- return fmt.Errorf("unknown artifact %q", name)
- } else {
- var suffix string
- if version := rosa.Std.Version(p); version != rosa.Unversioned {
- suffix += "-" + version
- }
- fmt.Println("name : " + name + suffix)
-
- meta := rosa.GetMetadata(p)
- fmt.Println("description : " + meta.Description)
- if meta.Website != "" {
- fmt.Println("website : " +
- strings.TrimSuffix(meta.Website, "/"))
- }
- if len(meta.Dependencies) > 0 {
- fmt.Print("depends on :")
- for _, d := range meta.Dependencies {
- s := rosa.GetMetadata(d).Name
- if version := rosa.Std.Version(d); version != rosa.Unversioned {
- s += "-" + version
- }
- fmt.Print(" " + s)
- }
- fmt.Println()
- }
-
- const statusPrefix = "status : "
- if flagStatus {
- if r == nil {
- var f io.ReadSeekCloser
- err = cm.Do(func(cache *pkg.Cache) (err error) {
- f, err = cache.OpenStatus(rosa.Std.Load(p))
- return
- })
- if err != nil {
- if errors.Is(err, os.ErrNotExist) {
- fmt.Println(
- statusPrefix + "not yet cured",
- )
- } else {
- return
- }
- } else {
- fmt.Print(statusPrefix)
- _, err = io.Copy(os.Stdout, f)
- if err = errors.Join(err, f.Close()); err != nil {
- return
- }
- }
- } else if err = cm.Do(func(cache *pkg.Cache) (err error) {
- status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
- if status == nil {
- fmt.Println(
- statusPrefix + "not in report",
- )
- } else {
- fmt.Println("size :", n)
- fmt.Print(statusPrefix)
- if _, err = os.Stdout.Write(status); err != nil {
- return
- }
- }
- return
- }); err != nil {
- return
- }
- }
-
- if i != len(args)-1 {
- fmt.Println()
- }
- }
- }
- return nil
+ return commandInfo(&cm, args, os.Stdout, flagStatus, flagReport)
},
).
Flag(