package index import ( "fmt" "maps" "slices" "sort" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) // The reasons a module has no fingerprint of the new form while the indexer writes none for any module (novox/hq // ADR 0465 clause 2; issue 1444). Toolchains missing is delivery.ToolchainNoReport. const ( // None2NoManifest is for a module whose directory the merged message carries no manifest of (an older bridge). None2NoManifest = "the message carries no manifest" // None2Pending is for a module with no artifact of a toolchain whose fingerprint the recompute of its chain gives (its // bases' fingerprints; no builder input since ADR 0530 ยง1). None2Pending = "the fingerprint is computed with its chain" // None2Context is for a module an artifact of which reads another repository's tree (today's form gives it none // either, NoneContext). None2Context = "context not pinned" ) // lockFile is the lock file of a language's artifact that resolves packages when it is built, by name in the module's // directory (the bridge reads these, internal/bridge LockFiles; issue 1450). var lockFile = map[string]string{"go": "go.sum", "typescript": "package-lock.json"} // moduleInputs is what a module's manifest and lock hashes give the indexer at a merge (issue 1450). type moduleInputs struct { // Kept is the new-form inputs the merge gave the module; nil when the manifest could not be read or gave none. Kept *delivery.KeptInputs2 // Languages is the language of each artifact that has a toolchain (a bundle or a package of go or typescript), by // artifact name. The toolchain versions are joined to them when the build modules report them (issue 1444, the // watching half); the SDK's KeptInputs2 keeps no languages, so they are read from the manifest again then. Languages map[string]string // None2 is why the module has no fingerprint of the new form. None2 string // ArtifactPaths are the paths each artifact naming them names, as its manifest names them (delivery.Module.ArtifactPaths). ArtifactPaths map[string][]string } // baseFP answers a base module's new-form fingerprint, or why it has none. type baseFP func(module string) (fp, none string) // inputsOf turns the manifest of a module (raw, as the forge answered it), the tree of its directory and the lock // hashes of that directory (lock file name to hash) into its new-form inputs. The manifest is read by the catalogue's // parser; a manifest it refuses, or one that is of another module, leaves Kept nil and says why. // // The rules are the controller's builder (mesh-controller internal/builder/builder.go): a bundle or a package of a // language with a toolchain (go, typescript) is the artifact that has one; a package, and a bundle of those languages, // resolves packages when it is built (wantsPackages, :1017) only when its language's lock file exists: it then has // an entry in Locks with that file's hash, and an artifact whose lock file the merge did not carry has no entry (a // dependency-free or vendored bundle resolves nothing, as mesh-sdk SourceInputs2 says of an artifact not in Locks; // novox/hq ADR 0469 clause 5, issue 1459). A bundle that needs a lock and lacks one is refused where it is built, not // here, so a fingerprint is never given for a build that could not run. An archive, an image and an upstream resolve // nothing and have no toolchain. A base is a build.on entry that names a module. func inputsOf(module, raw, tree string, locks map[string]string, base baseFP) moduleInputs { return inputsOfNamed(module, raw, tree, locks, nil, base) } // inputsOfNamed is inputsOf with the git object of each path the message names for the module's directory (delivery // Forge.Paths, PreviewModule.Paths). A module whose every artifact names its paths takes its tree input from them and its // manifest, by mesh-sdk's one function (delivery.NamedTree), and keeps their objects, so a computation from kept inputs // gives the merge's value (novox/hq ADR 0499 clause 2); a message that does not carry the object of each such path gives it // none, naming the paths (clause 3). Every other module keeps its directory's tree, whatever objects are given. func inputsOfNamed(module, raw, tree string, locks, objects map[string]string, base baseFP) moduleInputs { m, err := catalogue.ParseManifestForInputs([]byte(raw)) if err != nil { return moduleInputs{None2: None2Unreadable + err.Error()} } if m.Module != module { return moduleInputs{None2: fmt.Sprintf("the manifest is of module %q, not %q", m.Module, module)} } var artifactPaths map[string][]string if m.Build != nil { for _, a := range m.Build.Artifacts { if len(a.Paths) > 0 { if artifactPaths == nil { artifactPaths = map[string][]string{} } artifactPaths[a.Name] = slices.Clone(a.Paths) } } } var named map[string]string if paths, every := m.NamedPaths(); every { var missing []string named = map[string]string{} for _, p := range paths { if objects[p] == "" { missing = append(missing, p) continue } named[p] = objects[p] } if len(missing) > 0 { return moduleInputs{None2: "the message carries no git object of the named paths " + strings.Join(missing, ", "), ArtifactPaths: artifactPaths} } if tree, err = delivery.NamedTree(delivery.ManifestDigest([]byte(raw)), named); err != nil { return moduleInputs{None2: err.Error(), ArtifactPaths: artifactPaths} } } if tree == "" { return moduleInputs{None2: "the merge names no tree for the module's directory", ArtifactPaths: artifactPaths} } kept := delivery.KeptInputs2{Tree: tree, Paths: named} contexts := false var missing []string langs := languagesOf(m) if m.Build != nil { for _, a := range m.Build.Artifacts { if a.Context != nil { contexts = true } } for _, name := range slices.Sorted(maps.Keys(langs)) { // A lock file the merge did not carry leaves no entry: the artifact resolves nothing (ADR 0469 clause 5). if h := locks[lockFile[langs[name]]]; h != "" { if kept.Locks == nil { kept.Locks = map[string]string{} } kept.Locks[name] = h } } for _, on := range basesOf(m) { if fp, none := base(on); fp != "" { kept.Bases = append(kept.Bases, fp) } else { missing = append(missing, fmt.Sprintf("the base %s has no fingerprint (%s)", on, none)) } } } sort.Strings(kept.Bases) sort.Strings(missing) out := moduleInputs{Kept: &kept, ArtifactPaths: artifactPaths} if len(langs) > 0 { out.Languages = langs } switch { case contexts: out.None2 = None2Context case len(missing) > 0: out.None2 = strings.Join(missing, "; ") case len(langs) > 0: out.None2 = delivery.ToolchainNoReport default: out.None2 = None2Pending } return out } // languagesOf is the language of each artifact of the manifest that has a toolchain (a bundle or a package of go or // typescript), by artifact name: the artifacts a build module's toolchain enters the fingerprint of. func languagesOf(m catalogue.Manifest) map[string]string { langs := map[string]string{} if m.Build == nil { return langs } for _, a := range m.Build.Artifacts { if a.Kind != catalogue.ArtifactBundle && a.Kind != catalogue.ArtifactPackage { continue } if _, ok := lockFile[a.Language]; ok { langs[a.Name] = a.Language } } return langs } // basesOf is the modules the manifest names as bases (its build.on entries that name a module), each once, sorted. func basesOf(m catalogue.Manifest) []string { if m.Build == nil { return nil } seen := map[string]bool{} var out []string for _, on := range m.Build.On { if on.Module != "" && !seen[on.Module] { seen[on.Module] = true out = append(out, on.Module) } } sort.Strings(out) return out }