25a7797820
- We always download from https - All external assets have a primary and a mirror URL. - We prioritize the sources as follows: bazel mirror, github/original source, our mirror - There is a way (see build_toolchain) to test the sources from the mirrors (done before CL submission). This adds a utility to upload files to the mirror in a consistent, scripted way. It includes a way to copy in parts of our bazel files (e.g. debs_to_install from toolchain/build_toolchain.bzl) to update many things at once. Our Bazel mirror (gs://skia-world-readable/bazel) is a Content Addressable Storage system, where the file name is based on the sha256sum of the contents (the same hash that Bazel uses). All files in it should be publicly accessible. Change-Id: Ida8b8e07d27a0a557bc49467ebbc86c806cabbd3 Bug: skia:12541 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/494478 Reviewed-by: Joe Gregorio <jcgregorio@google.com>
158 lines
5.1 KiB
Go
158 lines
5.1 KiB
Go
// Copyright 2022 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// This executable downloads, verifies, and uploads a given file to the Skia infra Bazel mirror.
|
|
// Users should have gsutil installed, on the PATH and authenticated.
|
|
// There are two modes of use:
|
|
// - Specify a single file via --url and --sha256.
|
|
// - Copy a JSON array of objects (or Starlark list of dictionaries) via standard in.
|
|
// This should only need to be called when we add new dependencies or update existing ones. Calling
|
|
// it with already archived files should be fine - the mirror is a CAS, so the update should be a
|
|
// no-op. The files will be uploaded to the mirror with some metadata about where they came from.
|
|
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/flynn/json5"
|
|
|
|
"go.skia.org/infra/go/skerr"
|
|
)
|
|
|
|
const (
|
|
gcsBucketAndPrefix = "gs://skia-world-readable/bazel/"
|
|
)
|
|
|
|
func main() {
|
|
var (
|
|
url = flag.String("url", "", "The single url to mirror. --sha256 must be set.")
|
|
sha256Hash = flag.String("sha256", "", "The sha256sum of the url to mirror. --url must also be set.")
|
|
jsonFromStdin = flag.Bool("json", false, "If set, read JSON from stdin that consists of a list of objects.")
|
|
)
|
|
flag.Parse()
|
|
|
|
if (*url != "" && *sha256Hash == "") || (*url == "" && *sha256Hash != "") {
|
|
flag.Usage()
|
|
fatalf("Must set both of or non of --url and --sha256")
|
|
} else if !*jsonFromStdin {
|
|
fatalf("Must specify --url and --sha256 or --json")
|
|
}
|
|
|
|
workDir, err := os.MkdirTemp("", "bazel_gcs")
|
|
if err != nil {
|
|
fatalf("Could not make temp directory: %s", err)
|
|
}
|
|
|
|
if *jsonFromStdin {
|
|
fmt.Println("Waiting for input on std in. Use Ctrl+D (EOF) when done copying and pasting the array.")
|
|
b, err := io.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
fatalf("Error while reading from stdin: %s", err)
|
|
}
|
|
if err := processJSON(workDir, b); err != nil {
|
|
fatalf("Could not process data from stdin: %s", err)
|
|
}
|
|
} else {
|
|
if err := processOne(workDir, *url, *sha256Hash); err != nil {
|
|
fatalf("Error while processing entry: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
type urlEntry struct {
|
|
SHA256 string `json:"sha256"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
func processJSON(workDir string, b []byte) error {
|
|
// We generally will be copying a list from Bazel files, written with Starlark (i.e. Pythonish).
|
|
// As a result, we need to turn the almost valid JSON array of objects into actually valid JSON.
|
|
// It is easier to just do string replacing rather than going line by line to remove the
|
|
// troublesome comments.
|
|
cleaned := fixStarlarkComments(b)
|
|
var entries []urlEntry
|
|
if err := json5.Unmarshal([]byte(cleaned), &entries); err != nil {
|
|
return skerr.Wrapf(err, "unmarshalling JSON")
|
|
}
|
|
for _, entry := range entries {
|
|
if err := processOne(workDir, entry.URL, entry.SHA256); err != nil {
|
|
return skerr.Wrapf(err, "while processing entry: %+v", entry)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// fixStarlarkComments replaces the Starlark comment symbol (#) with a JSON comment symbol (//).
|
|
func fixStarlarkComments(b []byte) string {
|
|
return strings.ReplaceAll(string(b), "#", "//")
|
|
}
|
|
|
|
func processOne(workDir, url, hash string) error {
|
|
suf := getSuffix(url)
|
|
if suf == "" {
|
|
return skerr.Fmt("%s is not a supported file type", url)
|
|
}
|
|
fmt.Printf("Downloading and verifying %s...\n", url)
|
|
res, err := http.Get(url)
|
|
if err != nil {
|
|
return skerr.Wrapf(err, "downloading %s", url)
|
|
}
|
|
contents, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
return skerr.Wrapf(err, "reading %s", url)
|
|
}
|
|
if err := res.Body.Close(); err != nil {
|
|
return skerr.Wrapf(err, "after reading %s", url)
|
|
}
|
|
// Verify
|
|
h := sha256.Sum256(contents)
|
|
if actual := hex.EncodeToString(h[:]); actual != hash {
|
|
return skerr.Fmt("Invalid hash of %s. %s != %s", url, actual, hash)
|
|
}
|
|
fmt.Printf("Uploading %s to GCS...\n", url)
|
|
// Write to disk so gsutil can access it
|
|
tmpFile := filepath.Join(workDir, hash+suf)
|
|
if err := os.WriteFile(tmpFile, contents, 0644); err != nil {
|
|
return skerr.Wrapf(err, "writing %d bytes to %s", len(contents), tmpFile)
|
|
}
|
|
// Upload using gsutil (which is assumed to be properly authed)
|
|
cmd := exec.Command("gsutil",
|
|
// Add custom metadata so we can figure out what the unrecognizable file name was created
|
|
// from. Custom metadata values must start with x-goog-meta-
|
|
"-h", "x-goog-meta-original-url:"+url,
|
|
"cp", tmpFile, gcsBucketAndPrefix+hash+suf)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return skerr.Wrapf(cmd.Run(), "uploading %s to GCS", tmpFile)
|
|
}
|
|
|
|
var supportedSuffixes = []string{".tar.gz", ".tar.xz", ".deb"}
|
|
|
|
// getSuffix returns the filetype suffix of the file if it is in the list of supported suffixes.
|
|
// Otherwise, it returns empty string.
|
|
func getSuffix(url string) string {
|
|
for _, suf := range supportedSuffixes {
|
|
if strings.HasSuffix(url, suf) {
|
|
return suf
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func fatalf(format string, args ...interface{}) {
|
|
// Ensure there is a newline at the end of the fatal message.
|
|
format = strings.TrimSuffix(format, "\n") + "\n"
|
|
fmt.Printf(format, args...)
|
|
os.Exit(1)
|
|
}
|