blob: 6b75000a0c8a99dfbfa73d16a2ce56caca78702a [file] [log] [blame]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"github.com/bazelbuild/bazel-gazelle/label"
)
// dependency is an external Go package/module, requested by the user of Fietsje directly or indirectly.
type dependency struct {
// importpath is the Go import path that was used to import this dependency.
importpath string
// version at which this dependency has been requested. This can be in any form that `go get` or the go module
// system understands.
version string
// locked is the 'resolved' version of a dependency, containing information about the dependency's hash, etc.
locked *locked
// parent is the dependency that pulled in this one, or nil if pulled in by the user.
parent *dependency
shelf *shelf
// Build specific settings passed to gazelle.
disableProtoBuild bool
forceBazelGeneration bool
buildTags []string
patches []string
prePatches []string
buildExtraArgs []string
// replace is an importpath that this dependency will replace. If this is set, this dependency will be visible
// in the build as 'importpath', but downloaded at 'replace'/'version'. This might be slighly confusing, but
// follows the semantics of what Gazelle exposes via 'replace' in 'go_repository'.
replace string
}
func (d *dependency) remoteImportpath() string {
if d.replace != "" {
return d.replace
}
return d.importpath
}
// locked is information about a dependency resolved from the go module system. It is expensive to get, and as such
// it is cached both in memory (as .locked in a dependency) and in the shelf.
type locked struct {
// bazelName is the external workspace name that Bazel should use for this dependency, eg. com_github_google_glog.
bazelName string
// sum is the gomod compatible checksum of the depdendency, egh1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=.
sum string
// semver is the gomod-compatible version of this dependency. If the dependency was requested by git hash that does
// not resolve to a particular release, this will be in the form of v0.0.0-20200520133742-deadbeefcafe.
semver string
}
// child creates a new child dependence for this dependency, ie. one where the 'parent' pointer points to the dependency
// on which this method is called.
func (d *dependency) child(importpath, version string) *dependency {
return &dependency{
importpath: importpath,
version: version,
shelf: d.shelf,
parent: d,
}
}
func (d *dependency) String() string {
if d.replace != "" {
return fmt.Sprintf("%s@%s (replacing %s)", d.replace, d.version, d.importpath)
}
return fmt.Sprintf("%s@%s", d.importpath, d.version)
}
// lock ensures that this dependency is locked, which means that it has been resolved to a particular, stable version
// and VCS details. We lock a dependency by either asking the go module subsystem (via a go module proxy or a download),
// or by consulting the shelf as a cache.
func (d *dependency) lock() error {
// If already locked in-memory, use that.
if d.locked != nil {
return nil
}
// If already locked in the shelf, use that.
if shelved := d.shelf.get(d.remoteImportpath(), d.version); shelved != nil {
d.locked = shelved
return nil
}
// Otherwise, download module.
semver, _, sum, err := d.download()
if err != nil {
return fmt.Errorf("could not download: %v", err)
}
// And resolve its bazelName.
name := label.ImportPathToBazelRepoName(d.importpath)
d.locked = &locked{
bazelName: name,
sum: sum,
semver: semver,
}
log.Printf("%s: locked to %s", d, d.locked)
// Save locked version to shelf.
d.shelf.put(d.remoteImportpath(), d.version, d.locked)
return d.shelf.save()
}
func (l *locked) String() string {
return fmt.Sprintf("%s@%s", l.bazelName, l.sum)
}
// download ensures that this dependency is download locally, and returns the download location and the dependency's
// gomod-compatible sum.
func (d *dependency) download() (version, dir, sum string, err error) {
goroot := os.Getenv("GOROOT")
if goroot == "" {
err = fmt.Errorf("GOROOT must be set")
return
}
goTool := filepath.Join(goroot, "bin", "go")
query := fmt.Sprintf("%s@%s", d.remoteImportpath(), d.version)
cmd := exec.Command(goTool, "mod", "download", "-json", "--", query)
out, err := cmd.Output()
if err != nil {
log.Printf("go mod returned: %q", out)
err = fmt.Errorf("go mod failed: %v", err)
return
}
var res struct{ Version, Sum, Dir string }
err = json.Unmarshal(out, &res)
if err != nil {
return
}
version = res.Version
dir = res.Dir
sum = res.Sum
return
}