blob: c5d9833a4cd607c2b8863aa148fa70db1bb58021 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package internal
import (
"context"
"errors"
"fmt"
"io"
"log"
"math/big"
"net/url"
"path"
"regexp"
"strconv"
"strings"
"cloud.google.com/go/storage"
"golang.org/x/exp/slices"
"google.golang.org/api/iterator"
"chromium.googlesource.com/chromiumos/platform/dev-util.git/contrib/fflash/internal/artifact"
"chromium.googlesource.com/chromiumos/platform/dev-util.git/contrib/fflash/internal/dut"
"chromium.googlesource.com/chromiumos/platform/dev-util.git/contrib/fflash/internal/misc"
)
// version like R{r}-{x}.{y}.{z} e.g. R109-15236.80.0
//
// In fflash, we refer to those as:
//
// R109-15236.80.0
// --- ----- -- -
// | | | |
// | | | Patch number
// | | Branch number
// | Build number
// Milestone number
//
// The full string is called the version.
type version struct {
r int // Milestone number
x int // Build number
y int // Branch number
z int // Patch number
}
var versionRegexp = regexp.MustCompile(`^R(\d+)-(\d+).(\d+)\.(\d+)$`)
func parseVersion(v string) (version, error) {
m := versionRegexp.FindStringSubmatch(v)
if m == nil {
return version{}, fmt.Errorf("cannot parse %q as version", v)
}
var nums [4]int
for i := range nums {
var err error
nums[i], err = strconv.Atoi(m[i+1])
if err != nil {
return version{}, fmt.Errorf("cannot parse %q as version: %v", v, err)
}
}
return version{nums[0], nums[1], nums[2], nums[3]}, nil
}
func (v version) branched() bool {
return v.y != 0
}
func (v version) less(w version, latestMilestone int) bool {
if v.r != w.r {
return v.r < w.r
}
// For the same release R###, prefer branched versions.
// For example R109-15236.80.0 should be preferred to R109-15237.0.0.
// See also b/259389997.
// The exception to this rule is if the milestone is the latest
// milestone which is on ToT and has not been branched to a release branch.
// In that case we should just pick the one with highest build number.
// See also b/271417619
if latestMilestone != v.r && v.branched() != w.branched() {
return w.branched()
}
if v.x != w.x {
return v.x < w.x
}
if v.y != w.y {
return v.y < w.y
}
return v.z < w.z
}
func (v version) String() string {
return fmt.Sprintf("R%d-%d.%d.%d", v.r, v.x, v.y, v.z)
}
func queryPrefix(ctx context.Context, c *storage.Client, q *storage.Query) ([]*storage.ObjectAttrs, error) {
var result []*storage.ObjectAttrs
if err := q.SetAttrSelection([]string{"Name"}); err != nil {
panic("SetAttrSelection failed")
}
objects := c.Bucket("chromeos-image-archive").Objects(ctx, q)
for {
attrs, err := objects.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, fmt.Errorf("error while executing query: %w", err)
}
result = append(result, attrs)
}
return result, nil
}
// GetLatestVersionWithPrefix finds the latest build with the given prefix on gs://chromeos-image-archive.
func GetLatestVersionWithPrefix(ctx context.Context, c *storage.Client, board, prefix string) (string, error) {
fullPrefix := fmt.Sprintf("%s-release/%s", board, prefix)
q := &storage.Query{
Delimiter: "/",
Prefix: fullPrefix,
}
objects, err := queryPrefix(ctx, c, q)
if err != nil {
return "", fmt.Errorf("cannot get versions available for gs://chromeos-image-archive/%s*", fullPrefix)
}
var versions []version
for _, attrs := range objects {
v, err := parseVersion(path.Base(attrs.Prefix))
if err != nil {
log.Printf("Cannot parse %q, ignoring: %v", attrs.Prefix, err)
continue
}
versions = append(versions, v)
}
latestMilestone, err := latestMilestone(ctx, c, board)
if err != nil {
return "", err
}
slices.SortFunc(versions, func(a, b version) bool { return b.less(a, latestMilestone) })
for _, v := range versions {
dir := fmt.Sprintf("%s-release/%s", board, v)
if _, err := resolveArtifactImages(ctx, c, "chromeos-image-archive", dir); err != nil {
log.Printf("ignoring version %q: %v", v, err)
continue
}
return v.String(), nil
}
return "", fmt.Errorf("no versions found for gs://chromeos-image-archive/%s*", fullPrefix)
}
// getLatestVersionForLATEST finds the latest version for LATEST file for board on gs://chromeos-image-archive.
// If isPrefix is true, looks for the LATEST files having the {latest} as their prefix.
func getLatestVersionForLATEST(ctx context.Context, c *storage.Client, board string, latest string, isPrefix bool) (string, error) {
name := fmt.Sprintf("%s-release/%s", board, latest)
var objects []*storage.ObjectAttrs
if isPrefix {
var err error
q := &storage.Query{Prefix: name}
objects, err = queryPrefix(ctx, c, q)
if err != nil {
return "", fmt.Errorf("cannot get LATEST file from gs://chromeos-image-archive/%s*", name)
}
} else {
objects = []*storage.ObjectAttrs{
{
Name: name,
},
}
}
var latestVersion version
for _, attrs := range objects {
latestFileObj := c.Bucket("chromeos-image-archive").Object(attrs.Name)
r, err := latestFileObj.NewReader(ctx)
if err != nil {
return "", fmt.Errorf("cannot open LATEST file %s: %w", misc.GsURI(latestFileObj), err)
}
rawVersion, err := io.ReadAll(r)
if err != nil {
return "", fmt.Errorf("cannot read from LATEST file %s: %w", misc.GsURI(latestFileObj), err)
}
version, err := parseVersion(string(rawVersion))
if err != nil {
return "", fmt.Errorf("cannot parse LATEST file %s: %w", misc.GsURI(latestFileObj), err)
}
// using the latest file to parse version so don't need to worry about milestone when comparing
// versions
if latestVersion.less(version, 0) {
latestVersion = version
}
}
if latestVersion == (version{}) {
return "", fmt.Errorf("no LATEST file found for gs://chromeos-image-archive/%s*", name)
}
return latestVersion.String(), nil
}
func latestMilestone(ctx context.Context, c *storage.Client, board string) (int, error) {
latestVersionStr, err := GetLatestVersion(ctx, c, board)
if err != nil {
return 0, fmt.Errorf("Cannot determine latest milestone %w", err)
}
latestVersion, err := parseVersion(latestVersionStr)
if err != nil {
return 0, fmt.Errorf("Cannot determine latest milestone %w", err)
}
return latestVersion.r, nil
}
// GetLatestVersionForMilestone finds the latest version for board and milestone on gs://chromeos-image-archive.
func GetLatestVersionForMilestone(ctx context.Context, c *storage.Client, board string, milestone int) (string, error) {
version, err := getLatestVersionForLATEST(ctx, c, board, fmt.Sprintf("LATEST-release-R%d-", milestone), true)
if err != nil {
log.Printf("%v, maybe the milestone is not branched yet. Retrying with prefix matching", err)
version, err = GetLatestVersionWithPrefix(ctx, c, board, fmt.Sprintf("R%d-", milestone))
}
return version, err
}
// GetLatestVersion finds the latest version for board on gs://chromeos-image-archive.
func GetLatestVersion(ctx context.Context, c *storage.Client, board string) (string, error) {
return getLatestVersionForLATEST(ctx, c, board, "LATEST-main", false)
}
type snapshotID struct {
milestone int
build int
snapshot int
buildbucket *big.Int
}
var snapshotIDRegexp = regexp.MustCompile(`^R(\d+)-(\d+).0.0-(\d+)-(\d+)$`)
func parseSnapshotID(s string) (snapshotID, error) {
m := snapshotIDRegexp.FindStringSubmatch(s)
if m == nil {
return snapshotID{}, errors.New("bad format")
}
var id snapshotID
var err error
if id.milestone, err = strconv.Atoi(m[1]); err != nil {
return snapshotID{}, fmt.Errorf("bad milestone %q", m[1])
}
if id.build, err = strconv.Atoi(m[2]); err != nil {
return snapshotID{}, fmt.Errorf("bad build %q", m[2])
}
if id.snapshot, err = strconv.Atoi(m[3]); err != nil {
return snapshotID{}, fmt.Errorf("bad snapshot %q", m[3])
}
if val, ok := new(big.Int).SetString(m[4], 10); !ok {
return snapshotID{}, fmt.Errorf("bad buildbucket %q", m[4])
} else {
id.buildbucket = val
}
return id, nil
}
func (sid snapshotID) less(b snapshotID) bool {
if sid.milestone != b.milestone {
return sid.milestone < b.milestone
}
if sid.build != b.build {
return sid.build < b.build
}
if sid.snapshot != b.snapshot {
return sid.snapshot < b.snapshot
}
return sid.buildbucket.Cmp(b.buildbucket) == -1
}
func (sid snapshotID) String() string {
return fmt.Sprintf("R%d-%d.0.0-%d-%s", sid.milestone, sid.build, sid.snapshot, sid.buildbucket)
}
// Given an artifact directory, figure out the image names.
func resolveArtifactImages(ctx context.Context, c *storage.Client, bucket, dir string) (*artifact.Artifact, error) {
art := &artifact.Artifact{
Bucket: bucket,
Dir: dir,
Images: artifact.ZstdImages,
}
err1 := dut.CheckArtifact(ctx, c, art)
if err1 == nil {
return art, nil
}
art = &artifact.Artifact{
Bucket: bucket,
Dir: dir,
Images: artifact.GzipImages,
}
err2 := dut.CheckArtifact(ctx, c, art)
if err2 == nil {
return art, nil
}
return nil, errors.Join(err1, err2)
}
// Given a version hint, try to find a snapshot build.
// For snapshot/postsubmit images, see also:
// https://docs.google.com/presentation/d/1IynhrQMDtH3dAjXJD3Aheb4ZTYveVwBAEPgf6TgKzUs/edit?resourcekey=0-epY9CwLiU9zLv4E3dDs0FA#slide=id.g150f95639fd_0_70
func findSnapshotAroundVersion(ctx context.Context, c *storage.Client, snapshotOrPostsubmit, board, versionOrPrefix string) (*artifact.Artifact, error) {
var tryVersions []string
if v, err := parseVersion(versionOrPrefix); err != nil {
log.Printf("Looking for %s builds with prefix %s", snapshotOrPostsubmit, versionOrPrefix)
tryVersions = []string{versionOrPrefix}
} else {
log.Printf("Looking for %s builds around %s", snapshotOrPostsubmit, v)
tryVersions = []string{
version{v.r, v.x, 0, 0}.String(),
// Also try one release build before.
version{v.r, v.x - 1, 0, 0}.String(),
}
}
buildDirectory := fmt.Sprintf("%s-%s", board, snapshotOrPostsubmit)
for _, v := range tryVersions {
q := &storage.Query{
Delimiter: "/",
Prefix: fmt.Sprintf("%s/%s", buildDirectory, v),
}
objects, err := queryPrefix(ctx, c, q)
if err != nil {
return nil, fmt.Errorf("cannot get snapshots available for %s: %w", v, err)
}
var snapshots []snapshotID
for _, obj := range objects {
basename := path.Base(obj.Prefix)
pid, err := parseSnapshotID(basename)
if err != nil {
return nil, fmt.Errorf("cannot parse %q as snapshot ID: %w", basename, err)
}
snapshots = append(snapshots, pid)
}
slices.SortFunc(snapshots, func(a, b snapshotID) bool { return b.less(a) })
for _, pid := range snapshots {
// Verify that the flash payload actually exist.
dir := fmt.Sprintf("%s/%s", buildDirectory, pid)
art, err := resolveArtifactImages(ctx, c, "chromeos-image-archive", dir)
if err != nil {
log.Printf("%s not valid: %s", pid, err)
continue
}
return art, nil
}
}
return nil, fmt.Errorf("no valid %s build found", snapshotOrPostsubmit)
}
func getFlashTarget(ctx context.Context, c *storage.Client, board string, opts *Options) (*artifact.Artifact, error) {
if opts.GS != "" {
url, err := url.Parse(opts.GS)
if err != nil {
return nil, err
}
return resolveArtifactImages(ctx, c, url.Host, strings.TrimPrefix(url.RequestURI(), "/"))
}
var gsVersion string
var err error
if opts.VersionString != "" {
if opts.Postsubmit || opts.Snapshot {
// Don't resolve the version and let findSnapshotAroundVersion() handle it.
gsVersion = opts.VersionString
} else {
// Resolve for release build
gsVersion, err = GetLatestVersionWithPrefix(ctx, c, board, opts.VersionString)
}
} else if opts.MilestoneNum != 0 {
gsVersion, err = GetLatestVersionForMilestone(ctx, c, board, opts.MilestoneNum)
} else {
gsVersion, err = GetLatestVersion(ctx, c, board)
}
if err != nil {
return nil, err
}
if opts.Snapshot {
return findSnapshotAroundVersion(ctx, c, "snapshot", board, gsVersion)
} else if opts.Postsubmit {
return findSnapshotAroundVersion(ctx, c, "postsubmit", board, gsVersion)
}
return resolveArtifactImages(ctx, c, "chromeos-image-archive", fmt.Sprintf("%s-release/%s", board, gsVersion))
}