194 lines
4.9 KiB
Go
194 lines
4.9 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package marketplace
|
|
|
|
import (
|
|
filepath "dappco.re/go/core/scm/internal/ax/filepathx"
|
|
json "dappco.re/go/core/scm/internal/ax/jsonx"
|
|
os "dappco.re/go/core/scm/internal/ax/osx"
|
|
"sort"
|
|
"strings"
|
|
|
|
core "dappco.re/go/core"
|
|
|
|
coreio "dappco.re/go/core/io"
|
|
coreerr "dappco.re/go/core/log"
|
|
"dappco.re/go/core/scm/manifest"
|
|
)
|
|
|
|
// IndexVersion is the current marketplace index format version.
|
|
const IndexVersion = 1
|
|
|
|
// Builder constructs a marketplace Index by crawling directories for
|
|
// core.json (compiled manifests) or .core/manifest.yaml files.
|
|
type Builder struct {
|
|
// Medium is the filesystem abstraction used for manifest reads.
|
|
// If nil, io.Local is used.
|
|
Medium coreio.Medium
|
|
|
|
// BaseURL is the prefix for constructing repository URLs, e.g.
|
|
// "https://forge.lthn.ai". When set, module Repo is derived as
|
|
// BaseURL + "/" + org + "/" + code + ".git".
|
|
BaseURL string
|
|
|
|
// Org is the default organisation used when constructing Repo URLs.
|
|
Org string
|
|
}
|
|
|
|
// BuildFromDirs scans each directory, then its immediate subdirectories, for
|
|
// core.json (preferred) or .core/manifest.yaml. Each valid manifest is added
|
|
// to the resulting Index as a Module.
|
|
// Usage: BuildFromDirs(...)
|
|
func (b *Builder) BuildFromDirs(dirs ...string) (*Index, error) {
|
|
var modules []Module
|
|
seen := make(map[string]bool)
|
|
|
|
for _, dir := range dirs {
|
|
if err := b.loadInto(&modules, seen, dir); err != nil {
|
|
core.Warn(core.Sprintf("marketplace: skipping %s: %v", filepath.Base(dir), err))
|
|
}
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return nil, coreerr.E("marketplace.Builder.BuildFromDirs", "read "+dir, err)
|
|
}
|
|
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
|
|
child := filepath.Join(dir, e.Name())
|
|
if err := b.loadInto(&modules, seen, child); err != nil {
|
|
core.Warn(core.Sprintf("marketplace: skipping %s: %v", e.Name(), err))
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(modules, func(i, j int) bool {
|
|
return modules[i].Code < modules[j].Code
|
|
})
|
|
|
|
return &Index{
|
|
Version: IndexVersion,
|
|
Modules: modules,
|
|
}, nil
|
|
}
|
|
|
|
func (b *Builder) loadInto(modules *[]Module, seen map[string]bool, dir string) error {
|
|
m, err := b.loadFromDir(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
if seen[m.Code] {
|
|
return nil
|
|
}
|
|
seen[m.Code] = true
|
|
|
|
mod := Module{
|
|
Code: m.Code,
|
|
Name: m.Name,
|
|
Repo: b.repoURL(m.Code),
|
|
SignKey: m.Sign,
|
|
}
|
|
*modules = append(*modules, mod)
|
|
return nil
|
|
}
|
|
|
|
// BuildFromManifests constructs an Index from pre-loaded manifests.
|
|
// This is useful when manifests have already been collected (e.g. from
|
|
// a Forge API crawl).
|
|
// Usage: BuildFromManifests(...)
|
|
func BuildFromManifests(manifests []*manifest.Manifest) *Index {
|
|
var modules []Module
|
|
seen := make(map[string]bool)
|
|
|
|
for _, m := range manifests {
|
|
if m == nil || m.Code == "" {
|
|
continue
|
|
}
|
|
if seen[m.Code] {
|
|
continue
|
|
}
|
|
seen[m.Code] = true
|
|
|
|
modules = append(modules, Module{
|
|
Code: m.Code,
|
|
Name: m.Name,
|
|
SignKey: m.Sign,
|
|
})
|
|
}
|
|
|
|
sort.Slice(modules, func(i, j int) bool {
|
|
return modules[i].Code < modules[j].Code
|
|
})
|
|
|
|
return &Index{
|
|
Version: IndexVersion,
|
|
Modules: modules,
|
|
}
|
|
}
|
|
|
|
// WriteIndex serialises an Index to JSON and writes it to the given path
|
|
// using the provided filesystem medium.
|
|
// Usage: WriteIndex(...)
|
|
func WriteIndex(m coreio.Medium, path string, idx *Index) error {
|
|
if err := m.EnsureDir(filepath.Dir(path)); err != nil {
|
|
return coreerr.E("marketplace.WriteIndex", "mkdir failed", err)
|
|
}
|
|
data, err := json.MarshalIndent(idx, "", " ")
|
|
if err != nil {
|
|
return coreerr.E("marketplace.WriteIndex", "marshal failed", err)
|
|
}
|
|
return m.Write(path, string(data))
|
|
}
|
|
|
|
// loadFromDir tries core.json first, then falls back to .core/manifest.yaml.
|
|
func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) {
|
|
medium := b.Medium
|
|
if medium == nil {
|
|
medium = coreio.Local
|
|
}
|
|
|
|
// Prefer compiled manifest (core.json).
|
|
coreJSON := filepath.Join(dir, "core.json")
|
|
if raw, err := medium.Read(coreJSON); err == nil {
|
|
cm, err := manifest.ParseCompiled([]byte(raw))
|
|
if err != nil {
|
|
return nil, coreerr.E("marketplace.Builder.loadFromDir", "parse core.json", err)
|
|
}
|
|
return &cm.Manifest, nil
|
|
}
|
|
|
|
// Fall back to source manifest.
|
|
manifestYAML := filepath.Join(dir, ".core", "manifest.yaml")
|
|
raw, err := medium.Read(manifestYAML)
|
|
if err != nil {
|
|
return nil, nil // No manifest — skip silently.
|
|
}
|
|
|
|
m, err := manifest.Parse([]byte(raw))
|
|
if err != nil {
|
|
return nil, coreerr.E("marketplace.Builder.loadFromDir", "parse manifest.yaml", err)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// repoURL constructs a module repository URL from the builder config.
|
|
func (b *Builder) repoURL(code string) string {
|
|
if b.Org == "" {
|
|
return ""
|
|
}
|
|
baseURL := b.BaseURL
|
|
if baseURL == "" {
|
|
baseURL = defaultForgeURL
|
|
}
|
|
baseURL = strings.TrimSuffix(baseURL, "/")
|
|
return baseURL + "/" + b.Org + "/" + code + ".git"
|
|
}
|