package rosa import ( "context" "encoding/json" "errors" "net/http" "runtime" "slices" "strconv" "strings" "sync" "sync/atomic" "unique" "hakurei.app/internal/pkg" ) // ArtifactH is a handle of the unique name of a prepared [pkg.Artifact]. type ArtifactH unique.Handle[string] // String returns the name of p. func (p ArtifactH) String() string { return unique.Handle[string](p).Value() } // MarshalJSON represents [ArtifactH] by its [Artifact.Name]. func (p ArtifactH) MarshalJSON() ([]byte, error) { return json.Marshal(p.String()) } // UnmarshalJSON resolves [ArtifactH] by its [Artifact.Name]. func (p *ArtifactH) UnmarshalJSON(data []byte) error { var name string if err := json.Unmarshal(data, &name); err != nil { return err } *p = ArtifactH(unique.Make(name)) return nil } // P represents multiple [ArtifactH]. type P []ArtifactH // Artifact is stage-agnostic immutable data with a deterministic resulting // [pkg.Artifact]. It can be created natively or through evaluation. type Artifact struct { f func(t Toolchain, s *S) (a pkg.Artifact, version string) // Unique package name. Name string `json:"name"` // Short user-facing description. Description string `json:"description"` // Project home page. Website string `json:"website,omitempty"` // Runtime dependencies. Dependencies P `json:"dependencies"` // Project identifier on [Anitya]. // // [Anitya]: https://release-monitoring.org/ ID int `json:"-"` // Whether to exclude from exported functions. Exclude bool `json:"exclude,omitempty"` // Optional custom version checking behaviour. latest func(v *Versions) string } // GetLatest returns the latest version described by v. func (meta *Artifact) GetLatest(v *Versions) string { if meta.latest != nil { return meta.latest(v) } return v.Latest } // Unversioned denotes an unversioned [Artifact]. const Unversioned = "\x00" // UnpopulatedIDError is returned by [Artifact.GetLatest] for an instance of // [Artifact] where ID is not populated. type UnpopulatedIDError struct{} func (UnpopulatedIDError) Unwrap() error { return errors.ErrUnsupported } func (UnpopulatedIDError) Error() string { return "Anitya ID is not populated" } // Versions are package versions returned by Anitya. type Versions struct { // The latest version for the project, as determined by the version sorting algorithm. Latest string `json:"latest_version"` // List of all versions that aren’t flagged as pre-release. Stable []string `json:"stable_versions"` // List of all versions stored, sorted from newest to oldest. All []string `json:"versions"` } // getStable returns the first Stable version, or Latest if that is unavailable. func (v *Versions) getStable() string { if len(v.Stable) == 0 { return v.Latest } return v.Stable[0] } // GetVersions returns versions fetched from Anitya. func (meta *Artifact) GetVersions(ctx context.Context) (*Versions, error) { if meta.ID == 0 { return nil, UnpopulatedIDError{} } var resp *http.Response if req, err := http.NewRequestWithContext( ctx, http.MethodGet, "https://release-monitoring.org/api/v2/versions/?project_id="+ strconv.Itoa(meta.ID), nil, ); err != nil { return nil, err } else { req.Header.Set("User-Agent", "Rosa/1.1") if resp, err = http.DefaultClient.Do(req); err != nil { return nil, err } } var v Versions err := json.NewDecoder(resp.Body).Decode(&v) return &v, errors.Join(err, resp.Body.Close()) } // A cachedArtifact holds [pkg.Artifact] and its corresponding version string. type cachedArtifact struct { a pkg.Artifact v string } const ( // OptSkipCheck skips running all test suites. OptSkipCheck = 1 << iota // OptLLVMNoLTO disables LTO in all [LLVM] stages. OptLLVMNoLTO ) // S holds a set of [Artifact]. type S struct { // [ArtifactH] to [Artifact]. artifacts sync.Map // Size of artifacts. artifactCount atomic.Uint64 // Target architecture. arch string // For initialising arch. archOnce sync.Once // Options for [pkg.Artifact] created against [S]. opts int // Cached [pkg.Artifact]. c [_toolchainEnd]sync.Map // URL of a Gentoo stage3 tarball. gentooStage3 string // Expected checksum of gentooStage3. gentooStage3Checksum pkg.Checksum } // Clone returns a copy of s. func (s *S) Clone() *S { v := S{arch: s.arch} s.artifacts.Range(func(key, value any) bool { v.artifacts.Store(key, value) v.artifactCount.Add(1) return true }) return &v } // wantsArch must be called before accessing arch. func (s *S) wantsArch() { s.archOnce.Do(func() { if s.arch == "" { s.arch = runtime.GOARCH } }) } // Arch returns the target architecture. func (s *S) Arch() string { s.wantsArch(); return s.arch } // Flags returns the current preset flags. func (s *S) Flags() int { return s.opts } // DropCaches arranges for all cached [pkg.Artifact] to be freed some time after // it returns. Must not be used concurrently with any other method. func (s *S) DropCaches(targetArch string, flags int) { if targetArch == "" { targetArch = runtime.GOARCH } s.arch = targetArch s.opts = flags for i := range s.c { s.c[i].Clear() } } // Get returns the address of the named [Artifact]. func (s *S) Get(p ArtifactH) (meta *Artifact) { s.wantsArch() v, ok := s.artifacts.Load(p) if ok { meta = v.(*Artifact) } return } // MustGet is like Get, but panics if the named [Artifact] is not registered. func (s *S) MustGet(p ArtifactH) (meta *Artifact) { meta = s.Get(p) if meta == nil { panic("artifact " + strconv.Quote(p.String()) + " not available") } return } // Load returns the resulting [pkg.Artifact] of [ArtifactH]. func (s *S) Load(t Toolchain, p ArtifactH) (pkg.Artifact, string) { s.wantsArch() e, ok := s.c[t].Load(p) if ok { r := e.(cachedArtifact) return r.a, r.v } meta := s.Get(p) if meta == nil { return nil, "" } var r cachedArtifact r.a, r.v = meta.f(t, s) s.c[t].Store(p, r) return r.a, r.v } // MustLoad is like Load, but panics if the named [Artifact] is not registered. func (s *S) MustLoad(t Toolchain, p ArtifactH) (pkg.Artifact, string) { a, version := s.Load(t, p) if a == nil { panic("artifact " + strconv.Quote(p.String()) + " not available") } return a, version } // Register arranges for a new [Artifact] to be cured under s. It returns false // if another [Artifact] is already registered under the same name. func (s *S) Register(meta *Artifact) bool { if meta.Name == "" { return false } p := ArtifactH(unique.Make(meta.Name)) _, ok := s.artifacts.LoadOrStore(p, meta) if !ok { s.artifactCount.Add(1) } return !ok } // MustRegister is like Register, but panics if registration fails. func (s *S) MustRegister(meta *Artifact) { if !s.Register(meta) { panic("attempting to register " + strconv.Quote(meta.Name) + " twice") } } // Count returns the number of [Artifact] registered to s. func (s *S) Count() int { return int(s.artifactCount.Load()) } // Collect returns all [ArtifactH] registered to s. func (s *S) Collect() (handles []ArtifactH) { handles = make([]ArtifactH, 0, s.Count()) s.artifacts.Range(func(key, _ any) bool { handles = append(handles, key.(ArtifactH)) return true }) slices.SortFunc(handles, func(a, b ArtifactH) int { return strings.Compare(a.String(), b.String()) }) return } // SetGentooStage3 sets the Gentoo stage3 tarball url and checksum. It panics // if given zero values or if these values have already been set. func (s *S) SetGentooStage3(url string, checksum pkg.Checksum) { if s.gentooStage3 != "" { panic(errors.New("attempting to set Gentoo stage3 url twice")) } if url == "" { panic(errors.New("attempting to set Gentoo stage3 url to the zero value")) } s.gentooStage3, s.gentooStage3Checksum = url, checksum s.DropCaches(s.Arch(), s.Flags()) }