Compare commits
3
Commits
7dcae7b471
...
c0235bee17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0235bee17 | ||
|
|
a2732cf8da | ||
|
|
a789eb3083 |
+3
-1
@@ -14,7 +14,9 @@ RUN touch mfer/mf.pb.go
|
|||||||
# Go half of fmt-check only: this image has no node, so no prettier. The
|
# Go half of fmt-check only: this image has no node, so no prettier. The
|
||||||
# markdown half runs in the mdfmt stage below.
|
# markdown half runs in the mdfmt stage below.
|
||||||
RUN make fmt-check-go
|
RUN make fmt-check-go
|
||||||
RUN make lint
|
# The linter directly, not `make lint`: script/lint builds this stage, and
|
||||||
|
# there is no docker inside this build.
|
||||||
|
RUN golangci-lint run --config .golangci.yml ./...
|
||||||
|
|
||||||
# Markdown/JSON format stage — prettier needs node, which the Go images
|
# Markdown/JSON format stage — prettier needs node, which the Go images
|
||||||
# do not have. node:22.17.0-bookworm-slim (2026-08-09); ships node
|
# do not have. node:22.17.0-bookworm-slim (2026-08-09); ships node
|
||||||
|
|||||||
@@ -58,9 +58,6 @@ fmt-check-md:
|
|||||||
hooks:
|
hooks:
|
||||||
@script/install-precommit
|
@script/install-precommit
|
||||||
|
|
||||||
devprereqs:
|
|
||||||
which golangci-lint || go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
|
|
||||||
|
|
||||||
mfer/mf.pb.go: mfer/mf.proto
|
mfer/mf.pb.go: mfer/mf.proto
|
||||||
cd mfer && go generate .
|
cd mfer && go generate .
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# mfer
|
# mfer
|
||||||
|
|
||||||
[mfer](https://git.eeqj.de/sneak/mfer) is a reference implementation library and
|
[mfer](https://git.eeqj.de/sneak/mfer) is a [WTFPL](https://wtfpl.net)-licensed
|
||||||
thin wrapper command-line utility written in [Go](https://golang.org) and first
|
(public domain) [Go](https://golang.org) library and command-line tool by
|
||||||
published in 2022 under the [WTFPL](https://wtfpl.net) (public domain) license.
|
[@sneak](https://sneak.berlin) that specifies and generates `.mf` manifest files
|
||||||
It specifies and generates `.mf` manifest files over a directory tree of files
|
over a directory tree to encapsulate metadata about the files — such as
|
||||||
to encapsulate metadata about them (such as cryptographic checksums or
|
cryptographic checksums and signatures over same — to aid in archiving,
|
||||||
signatures over same) to aid in archiving, downloading, and streaming, or
|
downloading, streaming, and mirroring. It was first published in 2022. The
|
||||||
mirroring. The manifest files' data is serialized with Google's
|
manifest files' data is serialized with Google's
|
||||||
[protobuf serialization format](https://developers.google.com/protocol-buffers).
|
[protobuf serialization format](https://developers.google.com/protocol-buffers).
|
||||||
The structure of these files can be found
|
The structure of these files can be found
|
||||||
[in the format specification](https://git.eeqj.de/sneak/mfer/src/branch/main/mfer/mf.proto)
|
[in the format specification](https://git.eeqj.de/sneak/mfer/src/branch/main/mfer/mf.proto)
|
||||||
@@ -21,6 +21,36 @@ This project was started by [@sneak](https://sneak.berlin) to scratch an itch in
|
|||||||
as a de-facto standard and be incorporated into other software. A compatible
|
as a de-facto standard and be incorporated into other software. A compatible
|
||||||
javascript library is planned.
|
javascript library is planned.
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
`mfer` builds from source with a Go 1.23+ toolchain. The generated protobuf code
|
||||||
|
is committed, so no `protoc` toolchain is required:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://git.eeqj.de/sneak/mfer.git
|
||||||
|
cd mfer
|
||||||
|
go build -o bin/mfer ./cmd/mfer
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate a manifest for a directory tree, verify it later, and fetch a published
|
||||||
|
tree by URL:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Write .index.mf, a manifest of the files under the current directory.
|
||||||
|
bin/mfer gen .
|
||||||
|
|
||||||
|
# Verify the files on disk against the manifest. Exits nonzero if any file
|
||||||
|
# is missing or corrupted.
|
||||||
|
bin/mfer check .index.mf
|
||||||
|
|
||||||
|
# Download and cryptographically verify a tree published over HTTP: mfer
|
||||||
|
# fetches <url>/index.mf, then downloads every file it lists.
|
||||||
|
bin/mfer fetch https://example.com/tree/
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `bin/mfer help` for the full command list, or `bin/mfer <command> --help`
|
||||||
|
for a single command's options.
|
||||||
|
|
||||||
# Build Status
|
# Build Status
|
||||||
|
|
||||||
CI runs `script/cibuild`, which builds the Docker image with `--no-cache`, so
|
CI runs `script/cibuild`, which builds the Docker image with `--no-cache`, so
|
||||||
@@ -35,9 +65,9 @@ standard: normalized scripts in `script/` are the entrypoints for the
|
|||||||
development workflow, and the Makefile targets are thin shims that call them. We
|
development workflow, and the Makefile targets are thin shims that call them. We
|
||||||
provide:
|
provide:
|
||||||
|
|
||||||
- `script/bootstrap` — install all dependencies (Go, golangci-lint, Go module
|
- `script/bootstrap` — install all dependencies (Go, Go module download, and
|
||||||
download, and node/yarn plus the prettier version pinned in
|
node/yarn plus the prettier version pinned in `package.json`/`yarn.lock`),
|
||||||
`package.json`/`yarn.lock`), idempotently
|
idempotently; golangci-lint is not installed, it runs only in Docker
|
||||||
- `script/setup` — make a fresh clone ready for development: runs
|
- `script/setup` — make a fresh clone ready for development: runs
|
||||||
`script/bootstrap`, then `script/install-precommit`
|
`script/bootstrap`, then `script/install-precommit`
|
||||||
- `script/projectname` — output the project name (`mfer`); used by other scripts
|
- `script/projectname` — output the project name (`mfer`); used by other scripts
|
||||||
@@ -47,9 +77,11 @@ provide:
|
|||||||
- `script/fuzz` — fuzz the manifest parser for one minute; run by hand
|
- `script/fuzz` — fuzz the manifest parser for one minute; run by hand
|
||||||
(`make fuzz`), never by CI, while `script/test` runs its committed seed corpus
|
(`make fuzz`), never by CI, while `script/test` runs its committed seed corpus
|
||||||
as ordinary tests
|
as ordinary tests
|
||||||
- `script/lint` — run `golangci-lint` and verify `gofmt` cleanliness
|
- `script/lint` — run `golangci-lint` in Docker: builds only the `lint` stage of
|
||||||
- `script/fmt` — format all code and docs (writes): `gofumpt`,
|
the `Dockerfile` (the Go format check, then the linter), uncached so it runs
|
||||||
`golangci-lint run --fix`, and `script/prettier --write`
|
every time, then removes the image
|
||||||
|
- `script/fmt` — format all code and docs (writes): `gofumpt` and
|
||||||
|
`script/prettier --write`
|
||||||
- `script/prettier` — run prettier over the repository's canonical file set
|
- `script/prettier` — run prettier over the repository's canonical file set
|
||||||
(Markdown and JSON, minus `.prettierignore`) in the given mode, `--write` or
|
(Markdown and JSON, minus `.prettierignore`) in the given mode, `--write` or
|
||||||
`--check`; the single definition of that file set, so `script/fmt` and
|
`--check`; the single definition of that file set, so `script/fmt` and
|
||||||
@@ -75,17 +107,18 @@ yet. Primary development happens on a privately-run Gitea instance at
|
|||||||
[tracked there](https://git.eeqj.de/sneak/mfer/issues).
|
[tracked there](https://git.eeqj.de/sneak/mfer/issues).
|
||||||
|
|
||||||
Changes must always be formatted with a standard `go fmt`, syntactically valid,
|
Changes must always be formatted with a standard `go fmt`, syntactically valid,
|
||||||
and must pass the linting defined in the repository (presently only the
|
and must pass the linting defined in the repository's `.golangci.yml`, which
|
||||||
`golangci-lint` defaults), which can be run with a `make lint`. The `main`
|
`make lint` runs in Docker. The `main` branch is protected and all changes must
|
||||||
branch is protected and all changes must be made via
|
be made via [pull requests](https://git.eeqj.de/sneak/mfer/pulls) and pass CI to
|
||||||
[pull requests](https://git.eeqj.de/sneak/mfer/pulls) and pass CI to be merged.
|
be merged. Any changes submitted to this project must also be
|
||||||
Any changes submitted to this project must also be
|
|
||||||
[WTFPL-licensed](https://wtfpl.net) to be considered.
|
[WTFPL-licensed](https://wtfpl.net) to be considered.
|
||||||
|
|
||||||
See [`REPO_POLICIES.md`](REPO_POLICIES.md) for detailed coding standards,
|
See [`REPO_POLICIES.md`](REPO_POLICIES.md) for detailed coding standards,
|
||||||
tooling requirements, and workflow conventions.
|
tooling requirements, and workflow conventions.
|
||||||
|
|
||||||
# Problem Statement
|
# Rationale
|
||||||
|
|
||||||
|
## The problem
|
||||||
|
|
||||||
Given a plain URL, there is no standard way to safely and programmatically
|
Given a plain URL, there is no standard way to safely and programmatically
|
||||||
download everything "under" that URL path. `wget -r` can traverse directory
|
download everything "under" that URL path. `wget -r` can traverse directory
|
||||||
@@ -109,7 +142,7 @@ Real issues I face:
|
|||||||
- when I download a large file via HTTP, I have no way of knowing if the file
|
- when I download a large file via HTTP, I have no way of knowing if the file
|
||||||
content is what it's supposed to be
|
content is what it's supposed to be
|
||||||
|
|
||||||
# Proposed Solution
|
## The solution
|
||||||
|
|
||||||
A standard, a manifest file format, and a tool for generating same.
|
A standard, a manifest file format, and a tool for generating same.
|
||||||
|
|
||||||
@@ -141,6 +174,28 @@ The manifest file would do several important things:
|
|||||||
- maybe a bittorrent chunklist for torrent client compatibility? perhaps a
|
- maybe a bittorrent chunklist for torrent client compatibility? perhaps a
|
||||||
top-level infohash for the whole manifest?
|
top-level infohash for the whole manifest?
|
||||||
|
|
||||||
|
# Design
|
||||||
|
|
||||||
|
The repository is split into a reusable library and a thin command-line wrapper
|
||||||
|
around it.
|
||||||
|
|
||||||
|
- `mfer/` is the reusable library and the heart of the project: it defines the
|
||||||
|
manifest format and implements building, scanning, checking, serialization,
|
||||||
|
and signing. The protobuf schema is `mfer/mf.proto`, and the generated code it
|
||||||
|
produces (`mfer/mf.pb.go`) is committed alongside it so the library builds
|
||||||
|
with `go get` and needs no `protoc` toolchain.
|
||||||
|
- `internal/cli/` holds the command implementations — `generate` (alias `gen`),
|
||||||
|
`check`, `freshen`, `export`, `list` (alias `ls`), `fetch`, and `version` —
|
||||||
|
that wire the library to the command-line interface.
|
||||||
|
- `internal/log/` provides the logging used by the commands and the library.
|
||||||
|
- `internal/bork/` provides the error the library returns when a manifest's
|
||||||
|
decompressed contents are not the size the manifest records.
|
||||||
|
- `cmd/mfer/` is the entrypoint: its `main` package runs `internal/cli` and
|
||||||
|
exits with the status it returns.
|
||||||
|
|
||||||
|
Everything under `internal/` is private to this repository; only the `mfer/`
|
||||||
|
package is intended for import by other software.
|
||||||
|
|
||||||
# Design Goals
|
# Design Goals
|
||||||
|
|
||||||
- Replace SHASUMS/SHASUMS.asc files
|
- Replace SHASUMS/SHASUMS.asc files
|
||||||
@@ -274,9 +329,9 @@ Open work, open design questions included, is tracked in this repo's issues:
|
|||||||
- Issues:
|
- Issues:
|
||||||
[https://git.eeqj.de/sneak/mfer/issues](https://git.eeqj.de/sneak/mfer/issues)
|
[https://git.eeqj.de/sneak/mfer/issues](https://git.eeqj.de/sneak/mfer/issues)
|
||||||
|
|
||||||
# Authors
|
# Author
|
||||||
|
|
||||||
- [@sneak <sneak@sneak.berlin>](mailto:sneak@sneak.berlin)
|
- [@sneak](https://sneak.berlin)
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
|
|||||||
+125
-3
@@ -5,6 +5,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
urfcli "github.com/urfave/cli/v2"
|
urfcli "github.com/urfave/cli/v2"
|
||||||
|
"sneak.berlin/go/mfer/internal/log"
|
||||||
"sneak.berlin/go/mfer/mfer"
|
"sneak.berlin/go/mfer/mfer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ const (
|
|||||||
testManifest = "/manifest.mf"
|
testManifest = "/manifest.mf"
|
||||||
testFlagBase = "--base"
|
testFlagBase = "--base"
|
||||||
testFlagNoExtra = "--no-extra-files"
|
testFlagNoExtra = "--no-extra-files"
|
||||||
|
testFlagVersion = "--version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errSimulatedWrite = errors.New("simulated write failure")
|
var errSimulatedWrite = errors.New("simulated write failure")
|
||||||
@@ -39,12 +42,20 @@ var errSimulatedWrite = errors.New("simulated write failure")
|
|||||||
var runMu sync.Mutex
|
var runMu sync.Mutex
|
||||||
|
|
||||||
// runCLI invokes RunWithOptions while holding runMu so parallel tests
|
// runCLI invokes RunWithOptions while holding runMu so parallel tests
|
||||||
// capture their own output.
|
// capture their own output. Before releasing the lock it points the
|
||||||
|
// process-global logger at io.Discard: other tests log outside the lock
|
||||||
|
// (manifest loads, scans), and those lines must not land in this run's
|
||||||
|
// buffers once it has returned and its test is reading them.
|
||||||
func runCLI(opts *RunOptions) int {
|
func runCLI(opts *RunOptions) int {
|
||||||
runMu.Lock()
|
runMu.Lock()
|
||||||
defer runMu.Unlock()
|
defer runMu.Unlock()
|
||||||
|
|
||||||
return RunWithOptions(opts)
|
exitCode := RunWithOptions(opts)
|
||||||
|
|
||||||
|
log.SetOutput(io.Discard, io.Discard)
|
||||||
|
log.Init()
|
||||||
|
|
||||||
|
return exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
@@ -102,7 +113,7 @@ func TestVersionCommand(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
opts := testOpts([]string{testApp, "version"}, fs)
|
opts := testOpts([]string{testApp, cmdVersion}, fs)
|
||||||
|
|
||||||
exitCode := runCLI(opts)
|
exitCode := runCLI(opts)
|
||||||
|
|
||||||
@@ -113,6 +124,117 @@ func TestVersionCommand(t *testing.T) {
|
|||||||
assert.Contains(t, stdout, "abc123")
|
assert.Contains(t, stdout, "abc123")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestVFlagCollision covers the -v/--verbose vs --version flag interaction
|
||||||
|
// (issue #64). Verbose owns -v; version answers to --version and -V. None of
|
||||||
|
// these invocations may produce a parser error, and the two ways of asking
|
||||||
|
// for the version must print the same thing.
|
||||||
|
func TestVFlagCollision(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Invocations that must print the version and exit 0.
|
||||||
|
versionCases := map[string][]string{
|
||||||
|
"long version flag": {testApp, testFlagVersion},
|
||||||
|
"short version flag": {testApp, "-V"},
|
||||||
|
"verbose then version": {testApp, "-v", testFlagVersion},
|
||||||
|
"long verbose and version": {testApp, "--verbose", testFlagVersion},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, args := range versionCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := testOpts(args, afero.NewMemMapFs())
|
||||||
|
exitCode := runCLI(opts)
|
||||||
|
|
||||||
|
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
||||||
|
assert.Contains(t, testStdout(t, opts), mfer.Version)
|
||||||
|
assert.NotContains(t, testStderr(t, opts), "two forms of the same flag")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invocations that must enable verbose and exit 0 without a parser error.
|
||||||
|
verboseCases := map[string][]string{
|
||||||
|
"short verbose flag": {testApp, "-v"},
|
||||||
|
"long verbose flag": {testApp, "--verbose"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, args := range verboseCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := testOpts(args, afero.NewMemMapFs())
|
||||||
|
exitCode := runCLI(opts)
|
||||||
|
|
||||||
|
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
||||||
|
assert.Contains(t, testStdout(t, opts), cmdGenerate,
|
||||||
|
"root should show help listing subcommands")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestVersionFlagAndCommandMatch asserts that "mfer --version" and
|
||||||
|
// "mfer version" produce identical output (issue #64).
|
||||||
|
func TestVersionFlagAndCommandMatch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
flagOpts := testOpts([]string{testApp, testFlagVersion}, afero.NewMemMapFs())
|
||||||
|
require.Equal(t, 0, runCLI(flagOpts))
|
||||||
|
|
||||||
|
cmdOpts := testOpts([]string{testApp, cmdVersion}, afero.NewMemMapFs())
|
||||||
|
require.Equal(t, 0, runCLI(cmdOpts))
|
||||||
|
|
||||||
|
assert.Equal(t, testStdout(t, flagOpts), testStdout(t, cmdOpts))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRootVerbosityFlags asserts that -q and -v given before any subcommand
|
||||||
|
// name take effect: in the root itself, and in the fetch and export
|
||||||
|
// subcommands (issue #64). It does not run in parallel: other tests log
|
||||||
|
// outside runMu (manifest loads, scans), and a line logged while one of these
|
||||||
|
// runs is in progress lands in its output.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // see above
|
||||||
|
func TestRootVerbosityFlags(t *testing.T) {
|
||||||
|
t.Run("quiet with no subcommand hides the banner", func(t *testing.T) {
|
||||||
|
loud := testOpts([]string{testApp}, afero.NewMemMapFs())
|
||||||
|
require.Equal(t, 0, runCLI(loud))
|
||||||
|
assert.Contains(t, testStdout(t, loud), banner)
|
||||||
|
|
||||||
|
quiet := testOpts([]string{testApp, "-q"}, afero.NewMemMapFs())
|
||||||
|
require.Equal(t, 0, runCLI(quiet))
|
||||||
|
assert.Contains(t, testStdout(t, quiet), cmdGenerate)
|
||||||
|
assert.NotContains(t, testStdout(t, quiet), banner)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quiet before fetch hides the banner", func(t *testing.T) {
|
||||||
|
opts := testOpts([]string{testApp, "-q", cmdFetch}, afero.NewMemMapFs())
|
||||||
|
|
||||||
|
assert.Equal(t, 1, runCLI(opts))
|
||||||
|
assert.Contains(t, testStderr(t, opts), errURLRequired.Error())
|
||||||
|
assert.NotContains(t, testStdout(t, opts), banner)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("verbose twice before fetch enables debug logging", func(t *testing.T) {
|
||||||
|
opts := testOpts([]string{testApp, "-v", "-v", cmdFetch}, afero.NewMemMapFs())
|
||||||
|
|
||||||
|
assert.Equal(t, 1, runCLI(opts))
|
||||||
|
assert.Contains(t, testStderr(t, opts), "fetchManifestOperation()")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quiet before export hides the manifest summary", func(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
manifest := buildTestManifest(t, map[string][]byte{"a.txt": []byte("a")})
|
||||||
|
require.NoError(t, afero.WriteFile(fs, testManifest, manifest, 0o644))
|
||||||
|
|
||||||
|
loud := testOpts([]string{testApp, cmdExport, testManifest}, fs)
|
||||||
|
require.Equal(t, 0, runCLI(loud))
|
||||||
|
assert.Contains(t, testStderr(t, loud), "loaded manifest")
|
||||||
|
|
||||||
|
quiet := testOpts([]string{testApp, "-q", cmdExport, testManifest}, fs)
|
||||||
|
require.Equal(t, 0, runCLI(quiet))
|
||||||
|
assert.NotContains(t, testStderr(t, quiet), "loaded manifest")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestHelpCommand(t *testing.T) {
|
func TestHelpCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
+45
-5
@@ -19,6 +19,7 @@ const (
|
|||||||
cmdCheck = "check"
|
cmdCheck = "check"
|
||||||
cmdExport = "export"
|
cmdExport = "export"
|
||||||
cmdFetch = "fetch"
|
cmdFetch = "fetch"
|
||||||
|
cmdVersion = "version"
|
||||||
|
|
||||||
flagProgress = "progress"
|
flagProgress = "progress"
|
||||||
|
|
||||||
@@ -68,6 +69,12 @@ func (mfa *CLIApp) VersionString() string {
|
|||||||
return mfer.Version
|
return mfer.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printVersion writes the version line shared by the --version flag and the
|
||||||
|
// version subcommand, so both produce identical output.
|
||||||
|
func (mfa *CLIApp) printVersion() {
|
||||||
|
_, _ = fmt.Fprintf(mfa.Stdout, "%s version %s\n", mfa.appname, mfa.VersionString())
|
||||||
|
}
|
||||||
|
|
||||||
func (mfa *CLIApp) printBanner() {
|
func (mfa *CLIApp) printBanner() {
|
||||||
if log.GetLevel() <= log.InfoLevel {
|
if log.GetLevel() <= log.InfoLevel {
|
||||||
_, _ = fmt.Fprintln(mfa.Stdout, banner)
|
_, _ = fmt.Fprintln(mfa.Stdout, banner)
|
||||||
@@ -78,20 +85,34 @@ func (mfa *CLIApp) printBanner() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setVerbosity sets the log level from -v and -q, given before the subcommand
|
||||||
|
// name (the root's copies), after it (the subcommand's own copies), or both.
|
||||||
|
// urfave/cli reads a flag from the nearest command that defines it, so each
|
||||||
|
// command in the lineage is asked. The highest -v count wins rather than the
|
||||||
|
// sum, because a subcommand without its own copies reads the root's.
|
||||||
func (mfa *CLIApp) setVerbosity(c *cli.Context) {
|
func (mfa *CLIApp) setVerbosity(c *cli.Context) {
|
||||||
_, present := os.LookupEnv("MFER_DEBUG")
|
_, present := os.LookupEnv("MFER_DEBUG")
|
||||||
|
|
||||||
|
verbosity := 0
|
||||||
|
quiet := false
|
||||||
|
|
||||||
|
for _, ctx := range c.Lineage() {
|
||||||
|
verbosity = max(verbosity, ctx.Count("verbose"))
|
||||||
|
quiet = quiet || ctx.Bool("quiet")
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case present:
|
case present:
|
||||||
log.EnableDebugLogging()
|
log.EnableDebugLogging()
|
||||||
case c.Bool("quiet"):
|
case quiet:
|
||||||
log.SetLevel(log.ErrorLevel)
|
log.SetLevel(log.ErrorLevel)
|
||||||
default:
|
default:
|
||||||
log.SetLevelFromVerbosity(c.Count("verbose"))
|
log.SetLevelFromVerbosity(verbosity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// commonFlags returns the flags shared by most commands (-v, -q)
|
// commonFlags returns the -v and -q flags taken by the root and by the
|
||||||
|
// generate, check, freshen and fetch subcommands.
|
||||||
func commonFlags() []cli.Flag {
|
func commonFlags() []cli.Flag {
|
||||||
return []cli.Flag{
|
return []cli.Flag{
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
@@ -259,6 +280,8 @@ func (mfa *CLIApp) exportCommand() *cli.Command {
|
|||||||
Usage: "Export manifest contents as JSON",
|
Usage: "Export manifest contents as JSON",
|
||||||
ArgsUsage: "[manifest file or URL]",
|
ArgsUsage: "[manifest file or URL]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
|
mfa.setVerbosity(c)
|
||||||
|
|
||||||
return mfa.exportManifestOperation(c)
|
return mfa.exportManifestOperation(c)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -266,10 +289,10 @@ func (mfa *CLIApp) exportCommand() *cli.Command {
|
|||||||
|
|
||||||
func (mfa *CLIApp) versionCommand() *cli.Command {
|
func (mfa *CLIApp) versionCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "version",
|
Name: cmdVersion,
|
||||||
Usage: "Show version",
|
Usage: "Show version",
|
||||||
Action: func(_ *cli.Context) error {
|
Action: func(_ *cli.Context) error {
|
||||||
_, _ = fmt.Fprintln(mfa.Stdout, mfa.VersionString())
|
mfa.printVersion()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -325,6 +348,21 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
log.SetOutput(mfa.Stdout, mfa.Stderr)
|
log.SetOutput(mfa.Stdout, mfa.Stderr)
|
||||||
log.Init()
|
log.Init()
|
||||||
|
|
||||||
|
// -v means verbose, not version. urfave/cli's built-in version flag
|
||||||
|
// claims -v by default, which made "mfer -v --version" fail to parse and
|
||||||
|
// gave -v a different meaning at the root than on the generate, check,
|
||||||
|
// freshen and fetch subcommands, where it means verbose. Verbose is the
|
||||||
|
// more common meaning of -v in tools that offer both, so -v means verbose
|
||||||
|
// at the root too and the version flag takes the capital -V.
|
||||||
|
// VersionFlag and VersionPrinter are urfave/cli package globals; run() is
|
||||||
|
// serialized in tests, so assigning them here is safe.
|
||||||
|
cli.VersionFlag = &cli.BoolFlag{
|
||||||
|
Name: cmdVersion,
|
||||||
|
Aliases: []string{"V"},
|
||||||
|
Usage: "print the version",
|
||||||
|
}
|
||||||
|
cli.VersionPrinter = func(_ *cli.Context) { mfa.printVersion() }
|
||||||
|
|
||||||
mfa.app = &cli.App{
|
mfa.app = &cli.App{
|
||||||
Name: mfa.appname,
|
Name: mfa.appname,
|
||||||
Usage: "Manifest generator",
|
Usage: "Manifest generator",
|
||||||
@@ -332,11 +370,13 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
EnableBashCompletion: true,
|
EnableBashCompletion: true,
|
||||||
Writer: mfa.Stdout,
|
Writer: mfa.Stdout,
|
||||||
ErrWriter: mfa.Stderr,
|
ErrWriter: mfa.Stderr,
|
||||||
|
Flags: commonFlags(),
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
if c.Args().Len() > 0 {
|
if c.Args().Len() > 0 {
|
||||||
return fmt.Errorf("%w %q", errUnknownCommand, c.Args().First())
|
return fmt.Errorf("%w %q", errUnknownCommand, c.Args().First())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mfa.setVerbosity(c)
|
||||||
mfa.printBanner()
|
mfa.printBanner()
|
||||||
|
|
||||||
return cli.ShowAppHelp(c)
|
return cli.ShowAppHelp(c)
|
||||||
|
|||||||
+1
-6
@@ -140,12 +140,7 @@ main() {
|
|||||||
|
|
||||||
# ---- Go repos ----
|
# ---- Go repos ----
|
||||||
if missing go; then pkg_install go golang go go; fi
|
if missing go; then pkg_install go golang go go; fi
|
||||||
# golangci-lint: packaged in nix, brew, and apk. On apt there is no
|
# No golangci-lint: script/lint runs it in Docker only.
|
||||||
# package: download a specific release archive from GitHub and
|
|
||||||
# verify its hash (verify_sha256), never curl | sh.
|
|
||||||
if missing golangci-lint; then
|
|
||||||
pkg_install golangci-lint golangci-lint golangci-lint golangci-lint
|
|
||||||
fi
|
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
# ---- Python repos ----
|
# ---- Python repos ----
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ main() {
|
|||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
ensure_pb
|
ensure_pb
|
||||||
gofumpt -l -w mfer internal cmd
|
gofumpt -l -w mfer internal cmd
|
||||||
golangci-lint run --fix
|
|
||||||
# Markdown and JSON, over the same file set script/fmt-check verifies.
|
# Markdown and JSON, over the same file set script/fmt-check verifies.
|
||||||
"$SCRIPT_DIR/prettier" --write
|
"$SCRIPT_DIR/prettier" --write
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -1,17 +1,20 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/lint: run the linter.
|
# script/lint: run golangci-lint, in Docker only. Builds the lint stage of
|
||||||
|
# the Dockerfile, whose build runs the linter, so a successful build is a
|
||||||
|
# clean lint. --no-cache because a cached build runs no linter. The image
|
||||||
|
# is removed afterwards, whatever the outcome.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
golangci-lint run
|
# Tagged per run, so concurrent runs never remove each other's image.
|
||||||
if [ -n "$(gofmt -l .)" ]; then
|
image="$("$SCRIPT_DIR/projectname")-lint:$$"
|
||||||
echo "gofmt: files need formatting:" >&2
|
# A failed build leaves no image, so there is nothing to remove then.
|
||||||
gofmt -l . >&2
|
trap 'docker image rm "$image" >/dev/null 2>&1 || true' EXIT INT TERM
|
||||||
exit 1
|
docker build --no-cache --target lint -t "$image" .
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user