manifest

package module
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: MIT Imports: 3 Imported by: 0

README

manifest

The vocabulary shared by pkg/scanner (the manifest reader) and pkg/writer (the manifest rewriter). It exists so the reading and writing halves of dispat's manifest support can never drift apart: both range over the same format list, spell dependency kinds the same way, and agree byte for byte on what a Docker reference is.

The surface, by concern:

  • Formats. Format names each of the thirty-five recognised manifest formats, Formats is the canonical list both halves prove they cover, and FormatOf maps a file name onto its format. IsRequirementsFile and IsDockerfile are the two name rules too wordy for a lookup table: requirements files match by whole words, and Dockerfiles match Dockerfile, Dockerfile.dev, api.Dockerfile and Containerfile while excluding prose extensions.
  • Kinds. Kind is the dependency field a declaration came from, with the four constants spelled like the fields themselves and the zero value standing for plain dependencies. ParseKind reads a spelling from a config file or a command line, accepting "dependencies" as the long form of the zero value.
  • Names. NameWords splits a file's base name into words, and NormalizePyName applies PEP 503, identically on both sides, which is what keeps Acme_Core and acme-core one package.
  • Docker. ParseImageRef reads an image reference into repository, tag and digest with the tag's byte span; ValidTag is the registry tag grammar; DockerfileRefs walks a Dockerfile's FROM, COPY --from and RUN --mount instructions with stage-alias scoping; ComposeService and ComposeIdentity decide which service's image names a compose file. These are parsers, and they live here rather than in the reader or the writer precisely so both get byte-identical offsets.
manifest.FormatOf("Dockerfile.dev")                 // FormatDockerfile, true
manifest.KindDevDependencies                        // the four dependency kinds, spelled like the fields
manifest.ParseKind("dependencies")                  // KindDependencies, true
manifest.IsRequirementsFile("requirements-dev.txt") // true
manifest.NormalizePyName("Acme_Core")               // "acme-core" (PEP 503)
manifest.ParseImageRef("ghcr.io/acme/api:1.2.0")    // repository, tag and the tag's span

No dependencies and no I/O: models and pure functions, plus the pure parsers above. Nothing here touches a filesystem.

Requirements

Go 1.25 or later.

Licence

MIT. See LICENSE.

Documentation

Overview

Package manifest holds the vocabulary the scanner and writer modules share: the dependency-field kinds a manifest declares and the file-name rules both sides must agree on. It exists so the reading and writing halves of dispat's manifest support can never drift apart on what a kind is called or which files count as manifests.

Index

Constants

This section is empty.

Variables

Formats lists every recognised format. Both halves range over it to prove they cover the same ground.

Functions

func ComposeIdentity

func ComposeIdentity(services []ComposeService) (repository, tag string)

ComposeIdentity picks the image a compose file declares as its own, and returns its repository and tag. Both are empty when the file declares no identity, which is the honest answer for a compose file that only wires third-party services together.

A compose file lists many images and says nothing about which one the folder ships, so the choice is made by two rules in order.

First, the service that both builds and tags: a service with a build section and a tagged image is producing that image here, which is as close to "this is my package" as compose gets. Second, when nothing builds, the tagged repository the most services name — a scaled service appears several times under one image, and the third-party ones it sits beside appear once each.

Ties in either rule go to the lowest service name. A YAML mapping has no order worth trusting, so the tie-break has to come from the data rather than from how it was decoded, or the answer would change between runs.

func IsDockerfile

func IsDockerfile(name string) bool

IsDockerfile reports a container build file. Unlike every other format here, this one has no fixed name and no extension: Docker takes the base name "Dockerfile", the convention for a variant is a suffix ("Dockerfile.dev"), and the convention for keeping several in one folder is a prefix ("api.Dockerfile"). Podman's "Containerfile" is the same format spelled differently and is accepted on the same terms.

The comparison ignores case, because "-f dockerfile" builds exactly as "-f Dockerfile" does and repositories spell it both ways.

func IsRequirementsFile

func IsRequirementsFile(name string) bool

IsRequirementsFile reports a pip requirements file: a .txt whose base name starts or ends with the word "requirements" (requirements.txt, requirements-dev.txt, dev-requirements.txt). A name merely containing the word somewhere in the middle (OLD-REQUIREMENTS-NOTES.txt) is prose, not a manifest.

func NameWords

func NameWords(base string) []string

NameWords splits a file's base name into its separator-delimited words.

func NormalizePyName

func NormalizePyName(name string) string

NormalizePyName renders a Python distribution name in its PEP 503 normalised form: lowercased, every run of "-", "_" and "." collapsed to a single "-", so "Acme_Core" and "acme.core" name the same package.

func PathSuffix added in v1.1.1

func PathSuffix(f Format) (string, bool)

PathSuffix returns the slash-anchored path a path-qualified format is always kept at, and whether the format has one at all.

The four that do are the formats whose base name means something else everywhere else, which is why their folder is part of the name rather than a place the author chose: a Unity project's settings are ProjectSettings/ProjectSettings.asset or they are not those settings. A caller deciding whether a manifest belongs to the folder it scanned needs that distinction, because such a file is nested and still the folder's own.

func ValidTag

func ValidTag(tag string) bool

ValidTag reports text a registry would accept as a tag: up to 128 characters of letters, digits, underscores, periods and dashes, not opening with a separator. A writer checks it before splicing so a version that cannot be a tag is refused outright instead of producing a file that no longer builds.

Types

type ComposeService

type ComposeService struct {
	// Name is the service's key in the services map.
	Name string
	// Image is the declared image reference, empty when the service declares
	// none.
	Image string
	// Builds reports a service that declares a build section.
	Builds bool
}

ComposeService is one Compose service reduced to the facts that decide which image a compose file is *about*: what it is called, what image it declares, and whether it builds that image here rather than pulling it.

The type exists so the two halves of the tooling can share the decision without sharing the extraction. The reader gets these facts from a YAML decode; the writer gets them from the line walk it needs anyway to splice a scalar in place. Both then call ComposeIdentity, so they cannot disagree about which service owns the file's version — a disagreement that would show up as dispat writing a version into a service it never read one from.

type DockerRef

type DockerRef struct {
	Line       int
	Start, End int
	Text       string
}

DockerRef is one image reference located inside a Dockerfile: which line holds it and which bytes of that line it occupies.

The offsets are what let the writer splice a tag without rebuilding the line around it, and they are why locating references lives here rather than in each half separately. The reader only needs to know *which* images a file depends on and the writer only needs to know *where* they are, but both must agree on the answer to the first question — a reference the reader counts as a dependency and the writer cannot find would silently never be reconciled.

func DockerfileRefs

func DockerfileRefs(lines []string) []DockerRef

DockerfileRefs finds every image reference a Dockerfile depends on.

Three instructions name an image. FROM names the base of a stage. COPY --from and RUN --mount=…,from= pull files out of one. Each may instead name an earlier stage, by its AS alias or by its position, and a stage is part of this file rather than something it depends on, so those are filtered out as they are met — along with `scratch`, which is a keyword.

Instructions may run across several physical lines through a trailing backslash, and a splice needs a physical line, so the walk tracks the instruction it is inside rather than joining the lines up first. Stage aliases come into scope as they are defined, which means an alias only shadows an image for the instructions below it. That is the scope the builder gives them, and the reason `FROM tools:1.0 AS first` above `FROM alpine:3 AS tools` still reports a dependency on tools:1.0.

type Format

type Format string

Format identifies one manifest file format. It exists so the reader and the writer agree on which files count as manifests and what each one is, instead of each keeping its own list of names. Those lists drifted before this type did the job: a format could be readable and silently unwritable, and adding one meant remembering to edit two modules.

const (
	FormatNpm             Format = "npm"              // package.json
	FormatGoMod           Format = "gomod"            // go.mod
	FormatCargo           Format = "cargo"            // Cargo.toml
	FormatPyProject       Format = "pyproject"        // pyproject.toml
	FormatRequirements    Format = "requirements"     // requirements*.txt
	FormatComposer        Format = "composer"         // composer.json
	FormatMaven           Format = "maven"            // pom.xml
	FormatMSBuildProject  Format = "msbuild-project"  // *.csproj, *.fsproj, *.vbproj
	FormatNuSpec          Format = "nuspec"           // *.nuspec
	FormatPackagesProps   Format = "packages-props"   // Directory.Packages.props
	FormatPackagesConfig  Format = "packages-config"  // packages.config
	FormatPubspec         Format = "pubspec"          // pubspec.yaml, pubspec.yml
	FormatPlist           Format = "plist"            // Info.plist
	FormatAndroidManifest Format = "android-manifest" // AndroidManifest.xml
	FormatGradleCatalog   Format = "gradle-catalog"   // libs.versions.toml
	FormatGradleBuild     Format = "gradle-build"     // build.gradle, build.gradle.kts
	FormatXcodeProject    Format = "xcode-project"    // project.pbxproj
	FormatPodfile         Format = "podfile"          // Podfile
	FormatPodspec         Format = "podspec"          // *.podspec
	FormatGemfile         Format = "gemfile"          // Gemfile
	FormatGemspec         Format = "gemspec"          // *.gemspec
	FormatDockerfile      Format = "dockerfile"       // Dockerfile, Dockerfile.*, *.Dockerfile
	FormatCompose         Format = "compose"          // compose.yaml, docker-compose.yml, ...

	// The game engines. Each keeps its version somewhere a package manager
	// would not look, and several of them declare dependencies beside it.
	FormatUnityPackages        Format = "unity-packages"         // Packages/manifest.json
	FormatUnityProjectSettings Format = "unity-project-settings" // ProjectSettings/ProjectSettings.asset
	FormatGodotProject         Format = "godot-project"          // project.godot
	FormatGodotPlugin          Format = "godot-plugin"           // plugin.cfg
	FormatGodotExportPresets   Format = "godot-export-presets"   // export_presets.cfg
	FormatUnrealProject        Format = "unreal-project"         // *.uproject
	FormatUnrealPlugin         Format = "unreal-plugin"          // *.uplugin
	FormatUnrealGameConfig     Format = "unreal-game-config"     // Config/DefaultGame.ini
	FormatUnrealEngineConfig   Format = "unreal-engine-config"   // Config/DefaultEngine.ini
	FormatDefoldProject        Format = "defold-project"         // game.project
	FormatO3DEProject          Format = "o3de-project"           // project.json
	FormatO3DEGem              Format = "o3de-gem"               // gem.json
)

The formats both halves recognise. The value is a stable identifier rather than a file name, because several names map onto one format.

func FormatOf

func FormatOf(name string) (Format, bool)

FormatOf resolves a file's base name onto its format: by exact name, then by extension, then by the two families whose names vary. A name that matches nothing is not a manifest.

func FormatOfPath added in v1.1.0

func FormatOfPath(path string) (Format, bool)

FormatOfPath resolves a file's path onto its format: first the formats only recognisable by where they sit, then FormatOf on the base name. The path may be relative or absolute, and may use either separator.

It is the entry point a walk uses, because four formats cannot be told from their base name alone. FormatOf stays the answer where only a name is known, and deliberately never learns those four: a bare manifest.json is a web app manifest, and treating it as Unity's would be a guess.

type ImageRef

type ImageRef struct {
	// Repository is everything before the tag: the registry, its optional
	// port, and the path. "redis", "ghcr.io/acme/api", "localhost:5000/api".
	Repository string
	// Tag is the declared tag, empty when the reference names none.
	Tag string
	// Digest is the declared digest ("sha256:..."), empty when the reference
	// pins none.
	Digest string
	// TagStart and TagEnd are the byte offsets of Tag inside the reference the
	// split was given, so a writer can splice the tag without rebuilding the
	// text around it. Both are -1 when there is no tag.
	TagStart, TagEnd int
}

ImageRef is one Docker image reference split into the parts a manifest declares. A reference packs up to four things into one string — registry, repository path, tag and digest — and the reader and the writer must agree byte for byte on where each one starts, so the split lives here rather than in either half.

The parts are kept exactly as written. No registry is inferred, no "latest" is filled in and "library/" is never prepended: a manifest declares what it declares, and a writer that normalised the text would rewrite lines nobody asked it to touch.

func ParseImageRef

func ParseImageRef(ref string) ImageRef

ParseImageRef splits an image reference.

Two separators do the work, and the order matters. The digest is cut at the first "@", because everything after it belongs to the digest. The tag is then the text after a ":" that comes *after the last* "/" — that one rule is what tells the port in "localhost:5000/api" apart from the tag in "redis:7.2", since a registry's port can only appear before the first slash and a tag only after the last one.

A reference is never rejected. Anything unrecognisable comes back as a bare repository with no tag, which is inert: nothing matches it and no writer can splice it.

func (ImageRef) HasTag

func (r ImageRef) HasTag() bool

HasTag reports a reference carrying a tag a writer could splice.

func (ImageRef) Interpolated

func (r ImageRef) Interpolated() bool

Interpolated reports a reference whose repository or tag defers to a build argument or an environment variable ("${BASE}:${TAG}", "$IMAGE"). The value is resolved outside the file, and writing a literal over it would sever the indirection it exists for.

func (ImageRef) Pinned

func (r ImageRef) Pinned() bool

Pinned reports a reference carrying a digest. The digest is what actually gets pulled, so the tag beside it is a label rather than a selector and rewriting it would leave the file claiming a version it does not use.

type Kind

type Kind string

Kind is the manifest dependency field a declaration came from (or an edit targets). The zero value is the plain `dependencies` field, mirroring the config model's dependency kinds, so the two convert by a cast.

const (
	KindDependencies         Kind = ""
	KindDevDependencies      Kind = "devDependencies"
	KindPeerDependencies     Kind = "peerDependencies"
	KindOptionalDependencies Kind = "optionalDependencies"
)

Dependency kinds, spelled exactly like the manifest fields they stand for.

func ParseKind

func ParseKind(word string) (Kind, bool)

ParseKind maps a dependency-field spelling onto its Kind. The four field names are accepted, with "dependencies" read as the long spelling of the zero value, so a caller carrying a word from a config file or a command line never has to know the zero value stands for the plain field.

func (Kind) String

func (k Kind) String() string

String implements fmt.Stringer, spelling the zero value out.

func (Kind) Valid

func (k Kind) Valid() bool

Valid reports whether k is one of the four dependency kinds.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL