Repository navigation
Expand file tree
/
Copy pathmain.go
More file actions
141 lines (134 loc) · 4.42 KB
/
Copy pathmain.go
File metadata and controls
141 lines (134 loc) · 4.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// EmbedLedger records and compares the assets selected by Go's embed resolver.
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"runtime"
"time"
)
const version = "1.0.0"
func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
func run(args []string, out, errOut io.Writer) int {
if len(args) == 0 || args[0] == "help" || args[0] == "--help" || args[0] == "-h" {
fmt.Fprintln(out, `EmbedLedger: review the files selected by go:embed.
Usage: embedledger <scan|snapshot|check> [flags] [./package ...]
embedledger version
scan prints the current inventory; snapshot saves a baseline; check reports drift.
Default package selection: ./... in the selected module. Dependencies are excluded.
Run a subcommand with --help to see its flags.
Exit codes: 0 success, 1 inventory drift, 2 invalid input or incomplete scan.`)
return 0
}
if args[0] == "version" && len(args) == 1 {
fmt.Fprintln(out, "embedledger "+version)
return 0
}
command := args[0]
if command != "scan" && command != "snapshot" && command != "check" {
fmt.Fprintf(errOut, "unknown command %q; use embedledger help\n", command)
return 2
}
fs := flag.NewFlagSet(command, flag.ContinueOnError)
fs.SetOutput(errOut)
var opt options
fs.StringVar(&opt.Dir, "dir", ".", "module directory (must contain go.mod)")
fs.StringVar(&opt.GOOS, "goos", runtime.GOOS, "target operating system")
fs.StringVar(&opt.GOARCH, "goarch", runtime.GOARCH, "target architecture")
fs.StringVar(&opt.CGO, "cgo", "0", "CGO_ENABLED: 0 or 1")
fs.StringVar(&opt.Tags, "tags", "", "comma separated build tags")
fs.BoolVar(&opt.Tests, "tests", false, "also inventory internal and external test assets")
fs.Int64Var(&opt.MaxBytes, "max-bytes", 1<<30, "maximum total asset bytes to read (default 1 GiB)")
file := fs.String("file", "embedledger.json", "baseline path, relative to --dir")
asJSON := fs.Bool("json", false, "print structured JSON")
force := fs.Bool("force", false, "allow snapshot to replace an existing baseline")
timeout := fs.Duration("timeout", time.Minute, "scan deadline, for example 60s or 5m (maximum 1h)")
if err := fs.Parse(args[1:]); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
if *force && command != "snapshot" {
fmt.Fprintln(errOut, "--force is only valid for snapshot")
return 2
}
if *timeout <= 0 || *timeout > time.Hour {
fmt.Fprintln(errOut, "--timeout must be greater than zero and at most 1h")
return 2
}
opt.Patterns = fs.Args()
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
var previous manifest
if command == "check" {
var err error
previous, err = readManifest(baselinePath(opt.Dir, *file))
if err != nil {
fmt.Fprintln(errOut, err)
return 2
}
}
current, err := collect(ctx, opt)
if err != nil {
fmt.Fprintln(errOut, err)
if errors.Is(err, context.DeadlineExceeded) {
fmt.Fprintln(errOut, "scan timed out; retry with a longer --timeout (for example --timeout 5m), or select fewer packages")
}
return 2
}
if command != "scan" {
if err = rejectEmbeddedBaseline(opt.Dir, *file, current); err != nil {
fmt.Fprintln(errOut, err)
return 2
}
}
switch command {
case "snapshot":
err = saveManifest(baselinePath(opt.Dir, *file), current, *force)
if err == nil {
if *asJSON {
err = writeJSON(out, current)
} else {
_, err = fmt.Fprintf(out, "Saved %d assets (%d bytes) to %q\nReview this file before committing it.\n", len(current.Assets), current.TotalBytes, *file)
}
}
case "scan":
if *asJSON {
err = writeJSON(out, current)
} else {
for _, a := range current.Assets {
fmt.Fprintf(out, "%s %8d %s %q\n", a.Kind, a.Bytes, a.SHA256[:12], a.Path)
}
_, err = fmt.Fprintf(out, "%d assets, %d bytes, %s/%s, CGO=%s\n", len(current.Assets), current.TotalBytes, current.Scope.GOOS, current.Scope.GOARCH, current.Scope.CGO)
}
case "check":
if !sameScope(previous.Scope, current.Scope) {
fmt.Fprintln(errOut, "baseline scope differs: use the same packages, target, tags and --tests setting, or review a new snapshot")
return 2
}
diff := compare(previous, current)
if *asJSON {
err = writeJSON(out, diff)
} else {
err = printDiff(out, diff)
}
if err == nil && !diff.OK {
return 1
}
}
if err != nil {
fmt.Fprintln(errOut, err)
return 2
}
return 0
}
func writeJSON(w io.Writer, value any) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(value)
}