Build from the selected module
This commit is contained in:
@@ -13,11 +13,28 @@ type Target struct {
|
||||
Arch string
|
||||
}
|
||||
|
||||
// Settings contains normalized inputs shared by build execution and evidence.
|
||||
type Settings struct {
|
||||
MainPackage string
|
||||
BinaryName string
|
||||
Targets []Target
|
||||
}
|
||||
|
||||
type Declaration struct {
|
||||
Target Target
|
||||
Filename string
|
||||
}
|
||||
|
||||
// NormalizeSettings copies build inputs and removes duplicate targets while
|
||||
// preserving their first occurrence.
|
||||
func NormalizeSettings(mainPackage, binaryName string, targets []Target) Settings {
|
||||
return Settings{
|
||||
MainPackage: mainPackage,
|
||||
BinaryName: binaryName,
|
||||
Targets: NormalizeTargets(targets),
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeTargets removes duplicate targets while preserving their first
|
||||
// occurrence.
|
||||
func NormalizeTargets(targets []Target) []Target {
|
||||
|
||||
@@ -17,6 +17,22 @@ func TestNormalizeTargetsPreservesOrderAndFirstOccurrence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSettingsKeepsBuildInputsAndNormalizesTargets(t *testing.T) {
|
||||
settings := NormalizeSettings("./cmd/app", "app", []Target{
|
||||
{OS: "linux", Arch: "amd64"},
|
||||
{OS: "windows", Arch: "arm64"},
|
||||
{OS: "linux", Arch: "amd64"},
|
||||
})
|
||||
want := Settings{
|
||||
MainPackage: "./cmd/app",
|
||||
BinaryName: "app",
|
||||
Targets: []Target{{OS: "linux", Arch: "amd64"}, {OS: "windows", Arch: "arm64"}},
|
||||
}
|
||||
if !reflect.DeepEqual(settings, want) {
|
||||
t.Fatalf("NormalizeSettings() = %#v, want %#v", settings, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpectedBuildUsesSharedNamesInTargetOrder(t *testing.T) {
|
||||
targets := []Target{{OS: "windows", Arch: "arm64"}, {OS: "linux", Arch: "amd64"}}
|
||||
want := []Declaration{
|
||||
|
||||
@@ -16,25 +16,20 @@ import (
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||
)
|
||||
|
||||
type Target = artifact.Target
|
||||
|
||||
type Spec struct {
|
||||
MainPackage string
|
||||
BinaryName string
|
||||
Targets []Target
|
||||
}
|
||||
type Spec = artifact.Settings
|
||||
|
||||
type Request struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
Version string
|
||||
Spec Spec
|
||||
OutputDir string
|
||||
Output io.Writer
|
||||
ToolOwner *gotool.Owner
|
||||
Identity releaseidentity.Identity
|
||||
Spec Spec
|
||||
OutputDir string
|
||||
Output io.Writer
|
||||
ToolOwner *gotool.Owner
|
||||
}
|
||||
|
||||
type Artifact struct {
|
||||
@@ -51,6 +46,7 @@ type Result struct {
|
||||
Tag string
|
||||
CandidateCommit string
|
||||
SourceVerified bool
|
||||
Settings artifact.Settings
|
||||
Version string
|
||||
MainPackage string
|
||||
BinaryName string
|
||||
@@ -60,25 +56,32 @@ type Result struct {
|
||||
Compiler string
|
||||
}
|
||||
|
||||
func Build(ctx context.Context, request Request) (Result, error) {
|
||||
func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
||||
identity := request.Identity
|
||||
settings := artifact.NormalizeSettings(request.Spec.MainPackage, request.Spec.BinaryName, request.Spec.Targets)
|
||||
result := Result{
|
||||
Version: request.Version,
|
||||
MainPackage: request.Spec.MainPackage,
|
||||
BinaryName: request.Spec.BinaryName,
|
||||
Targets: artifact.NormalizeTargets(request.Spec.Targets),
|
||||
RepositoryDir: identity.RepositoryDir,
|
||||
ModuleDir: identity.ModuleDir,
|
||||
ModulePath: identity.ModulePath,
|
||||
Tag: identity.Tag,
|
||||
Settings: settings,
|
||||
Version: identity.Version,
|
||||
MainPackage: settings.MainPackage,
|
||||
BinaryName: settings.BinaryName,
|
||||
Targets: settings.Targets,
|
||||
}
|
||||
if !versioncontract.ValidStableVersion(request.Version) {
|
||||
return result, fmt.Errorf("%w: %q", candidate.ErrInvalidVersion, request.Version)
|
||||
if !versioncontract.ValidStableVersion(identity.Version) {
|
||||
return result, fmt.Errorf("%w: %q", candidate.ErrInvalidVersion, identity.Version)
|
||||
}
|
||||
if request.RepositoryDir == "" {
|
||||
return result, errors.New("build requires a repository directory")
|
||||
if identity.RepositoryDir == "" || identity.ModuleDir == "" || identity.ModulePath == "" {
|
||||
return result, errors.New("build requires a resolved module identity")
|
||||
}
|
||||
if len(result.Targets) != 0 {
|
||||
if request.Spec.MainPackage == "" {
|
||||
if settings.MainPackage == "" {
|
||||
return result, errors.New("build requires a main package when targets are selected")
|
||||
}
|
||||
if !artifact.ValidComponent(request.Spec.BinaryName) {
|
||||
return result, fmt.Errorf("unsafe binary name %q", request.Spec.BinaryName)
|
||||
if !artifact.ValidComponent(settings.BinaryName) {
|
||||
return result, fmt.Errorf("unsafe binary name %q", settings.BinaryName)
|
||||
}
|
||||
for _, target := range result.Targets {
|
||||
if !artifact.ValidComponent(target.OS) || !artifact.ValidComponent(target.Arch) {
|
||||
@@ -89,11 +92,16 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return result, fmt.Errorf("build canceled: %w", err)
|
||||
}
|
||||
repositoryDir, err := filepath.Abs(request.RepositoryDir)
|
||||
repositoryDir, err := filepath.Abs(identity.RepositoryDir)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("resolve repository directory: %w", err)
|
||||
}
|
||||
result.RepositoryDir = repositoryDir
|
||||
moduleDir, err := filepath.Abs(identity.ModuleDir)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("resolve module directory: %w", err)
|
||||
}
|
||||
result.ModuleDir = moduleDir
|
||||
commit, err := gitCommit(ctx, repositoryDir)
|
||||
if err != nil {
|
||||
return result, err
|
||||
@@ -105,12 +113,12 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
||||
}
|
||||
|
||||
if len(result.Targets) == 0 {
|
||||
result.OutputDir = resolvedOutputDir(repositoryDir, request.OutputDir)
|
||||
result.OutputDir = resolvedOutputDir(moduleDir, request.OutputDir)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result.OutputDir = resolvedOutputDir(repositoryDir, request.OutputDir)
|
||||
declarations := artifact.ExpectedBuild(request.Spec.BinaryName, request.Version, result.Targets)
|
||||
result.OutputDir = resolvedOutputDir(moduleDir, request.OutputDir)
|
||||
declarations := artifact.ExpectedBuild(settings.BinaryName, identity.Version, settings.Targets)
|
||||
filenames := make([]string, len(declarations))
|
||||
for i, declaration := range declarations {
|
||||
filenames[i] = declaration.Filename
|
||||
@@ -121,13 +129,13 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
||||
|
||||
owner := request.ToolOwner
|
||||
if owner == nil {
|
||||
owner = gotool.NewOwner(repositoryDir)
|
||||
owner = gotool.NewOwner(moduleDir)
|
||||
}
|
||||
goTool, err := owner.Acquire(ctx)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("prepare Go toolchain: %w", err)
|
||||
}
|
||||
compiler, err := goTool.Output(ctx, repositoryDir, gotool.Native, gotool.Target{}, nil, "version")
|
||||
compiler, err := goTool.Output(ctx, moduleDir, gotool.Native, gotool.Target{}, nil, "version")
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("identify Go compiler: %w", err)
|
||||
}
|
||||
@@ -144,7 +152,7 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
||||
if request.Output != nil {
|
||||
fmt.Fprintf(request.Output, "building %s/%s as %s\n", target.OS, target.Arch, filename)
|
||||
}
|
||||
if err := buildTarget(ctx, goTool, repositoryDir, request.Spec.MainPackage, path, request.Version, target); err != nil {
|
||||
if err := buildTarget(ctx, goTool, moduleDir, settings.MainPackage, path, identity.Version, target); err != nil {
|
||||
return result, fmt.Errorf("build target %s/%s: %w", target.OS, target.Arch, err)
|
||||
}
|
||||
artifact := Artifact{Path: path, Filename: filename, Target: target}
|
||||
@@ -162,26 +170,26 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
||||
}
|
||||
}
|
||||
if smokePath == "" {
|
||||
temporaryDir, err := os.MkdirTemp("", "releasekit-native-smoke-")
|
||||
temporaryDir, err := tempdir.Outside(repositoryDir, "releasekit-native-smoke-")
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("create native smoke directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(temporaryDir)
|
||||
smokePath = filepath.Join(temporaryDir, request.Spec.BinaryName)
|
||||
smokePath = filepath.Join(temporaryDir, settings.BinaryName)
|
||||
if host.OS == "windows" {
|
||||
smokePath += ".exe"
|
||||
}
|
||||
if request.Output != nil {
|
||||
fmt.Fprintf(request.Output, "building temporary version check for %s/%s\n", host.OS, host.Arch)
|
||||
}
|
||||
if err := buildTarget(ctx, goTool, repositoryDir, request.Spec.MainPackage, smokePath, request.Version, host); err != nil {
|
||||
if err := buildTarget(ctx, goTool, moduleDir, settings.MainPackage, smokePath, identity.Version, host); err != nil {
|
||||
return result, fmt.Errorf("build native version check: %w", err)
|
||||
}
|
||||
}
|
||||
if request.Output != nil {
|
||||
fmt.Fprintf(request.Output, "checking %s version output\n", request.Spec.BinaryName)
|
||||
fmt.Fprintf(request.Output, "checking %s version output\n", settings.BinaryName)
|
||||
}
|
||||
if err := checkVersion(ctx, smokePath, request.Spec.BinaryName, request.Version); err != nil {
|
||||
if err := checkVersion(ctx, smokePath, settings.BinaryName, identity.Version); err != nil {
|
||||
return result, err
|
||||
}
|
||||
commitAfter, err := gitCommit(ctx, repositoryDir)
|
||||
@@ -196,11 +204,11 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func resolvedOutputDir(repositoryDir, outputDir string) string {
|
||||
func resolvedOutputDir(moduleDir, outputDir string) string {
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Join(repositoryDir, "dist")
|
||||
outputDir = filepath.Join(moduleDir, "dist")
|
||||
} else if !filepath.IsAbs(outputDir) {
|
||||
outputDir = filepath.Join(repositoryDir, outputDir)
|
||||
outputDir = filepath.Join(moduleDir, outputDir)
|
||||
}
|
||||
resolved, err := filepath.Abs(outputDir)
|
||||
if err != nil {
|
||||
@@ -221,9 +229,9 @@ func gitCommit(ctx context.Context, repositoryDir string) (string, error) {
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
||||
func buildTarget(ctx context.Context, goTool *gotool.Tool, repositoryDir, mainPackage, outputPath, version string, target Target) error {
|
||||
func buildTarget(ctx context.Context, goTool *gotool.Tool, moduleDir, mainPackage, outputPath, version string, target Target) error {
|
||||
ldflags := "-s -w -X " + versioncontract.InjectedVersionLinkerSymbol + "=" + version
|
||||
err := goTool.Run(ctx, repositoryDir, gotool.Static, gotool.Target{OS: target.OS, Arch: target.Arch}, nil,
|
||||
err := goTool.Run(ctx, moduleDir, gotool.Static, gotool.Target{OS: target.OS, Arch: target.Arch}, nil,
|
||||
"build", "-o", outputPath, "-mod=readonly", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, mainPackage)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
||||
)
|
||||
|
||||
@@ -18,18 +20,32 @@ func CandidateCallback(spec *Spec, output io.Writer) func(context.Context, candi
|
||||
selected := *spec
|
||||
selected.Targets = append([]Target(nil), spec.Targets...)
|
||||
return func(ctx context.Context, captured candidate.Candidate, owner *gotool.Owner) error {
|
||||
moduleSubdir, err := filepath.Rel(captured.RepositoryDir, captured.ModuleDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve captured module path: %w", err)
|
||||
}
|
||||
identity := releaseidentity.Identity{
|
||||
Selection: releaseidentity.Selection{
|
||||
RepositoryDir: captured.RepositoryDir,
|
||||
ModuleDir: captured.ModuleDir,
|
||||
ModuleSubdir: filepath.ToSlash(moduleSubdir),
|
||||
},
|
||||
ModulePath: captured.ModulePath,
|
||||
Version: captured.Version,
|
||||
Tag: captured.Tag,
|
||||
NotePath: filepath.ToSlash(filepath.Join(moduleSubdir, "docs", "releases", captured.Version+".md")),
|
||||
}
|
||||
temporaryDir, err := tempdir.Outside(captured.RepositoryDir, "releasekit-candidate-build-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temporary candidate build output: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(temporaryDir)
|
||||
_, err = Build(ctx, Request{
|
||||
RepositoryDir: captured.RepositoryDir,
|
||||
Version: captured.Version,
|
||||
Spec: selected,
|
||||
OutputDir: temporaryDir,
|
||||
Output: output,
|
||||
ToolOwner: owner,
|
||||
_, err = BuildResolved(ctx, Request{
|
||||
Identity: identity,
|
||||
Spec: selected,
|
||||
OutputDir: temporaryDir,
|
||||
Output: output,
|
||||
ToolOwner: owner,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("build candidate: %w", err)
|
||||
|
||||
@@ -11,16 +11,17 @@ import (
|
||||
)
|
||||
|
||||
func declaredBuildAssets(result internalbuild.Result) ([]gitea.ExpectedAsset, error) {
|
||||
if len(result.Targets) == 0 {
|
||||
settings := result.Settings
|
||||
if len(settings.Targets) == 0 {
|
||||
if len(result.Artifacts) != 0 {
|
||||
return nil, fmt.Errorf("source-only build result contains artifacts")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if len(result.Artifacts) != len(result.Targets) {
|
||||
if len(result.Artifacts) != len(settings.Targets) {
|
||||
return nil, fmt.Errorf("build result must contain one artifact for each selected target")
|
||||
}
|
||||
declarations := artifact.ExpectedBuild(result.BinaryName, result.Version, result.Targets)
|
||||
declarations := artifact.ExpectedBuild(settings.BinaryName, result.Version, settings.Targets)
|
||||
wantByTarget := make(map[artifact.Target]string, len(declarations))
|
||||
for _, declaration := range declarations {
|
||||
target := declaration.Target
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
||||
)
|
||||
@@ -25,8 +26,10 @@ func TestDeclaredBuildAssetsGenerateManifestFromBuildResult(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := declaredBuildAssets(internalbuild.Result{
|
||||
Version: "v1.2.3", BinaryName: "app",
|
||||
Targets: []internalbuild.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}},
|
||||
Version: "v1.2.3",
|
||||
Settings: artifact.NormalizeSettings("./cmd/app", "app", []artifact.Target{
|
||||
{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"},
|
||||
}),
|
||||
Artifacts: []internalbuild.Artifact{
|
||||
{Path: firstPath, Filename: filepath.Base(firstPath), Target: internalbuild.Target{OS: "linux", Arch: "amd64"}, SHA256: assetDigest(first)},
|
||||
{Path: secondPath, Filename: filepath.Base(secondPath), Target: internalbuild.Target{OS: "darwin", Arch: "arm64"}, SHA256: assetDigest(second)},
|
||||
@@ -54,7 +57,7 @@ func TestDeclaredBuildAssetsRejectsArtifactDeclarationMismatch(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := declaredBuildAssets(internalbuild.Result{
|
||||
Version: "v1.2.3", BinaryName: "app", Targets: []internalbuild.Target{{OS: "linux", Arch: "amd64"}},
|
||||
Version: "v1.2.3", Settings: artifact.NormalizeSettings("./cmd/app", "app", []artifact.Target{{OS: "linux", Arch: "amd64"}}),
|
||||
Artifacts: []internalbuild.Artifact{{
|
||||
Path: path, Filename: filepath.Base(path), Target: internalbuild.Target{OS: "linux", Arch: "amd64"}, SHA256: assetDigest(contents),
|
||||
}},
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||
)
|
||||
@@ -90,17 +91,24 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
||||
|
||||
build := request.Prebuilt
|
||||
if request.Build != nil && len(request.Build.Targets) != 0 {
|
||||
selection, err := releaseidentity.ResolveSelection(repositoryDir, request.ModuleDir)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("resolve managed build module: %w", err)
|
||||
}
|
||||
identity, err := releaseidentity.FromCommittedSelection(ctx, selection, request.Version, source.TagCommit)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("resolve managed build identity: %w", err)
|
||||
}
|
||||
temporaryDir, err := tempdir.Outside(repositoryDir, "releasekit-delivery-build-*")
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("create temporary delivery build directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(temporaryDir)
|
||||
built, err := internalbuild.Build(ctx, internalbuild.Request{
|
||||
RepositoryDir: repositoryDir,
|
||||
Version: request.Version,
|
||||
Spec: *request.Build,
|
||||
OutputDir: filepath.Join(temporaryDir, "assets"),
|
||||
Output: request.Output,
|
||||
built, err := internalbuild.BuildResolved(ctx, internalbuild.Request{
|
||||
Identity: identity,
|
||||
Spec: *request.Build,
|
||||
OutputDir: filepath.Join(temporaryDir, "assets"),
|
||||
Output: request.Output,
|
||||
})
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("build delivery assets: %w", err)
|
||||
@@ -122,7 +130,7 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
||||
return result, fmt.Errorf("verify build assets: %w", err)
|
||||
}
|
||||
}
|
||||
if build != nil && len(build.Targets) != 0 {
|
||||
if build != nil && len(build.Settings.Targets) != 0 {
|
||||
current, err := git.InspectTaggedSource(ctx, repositoryDir, request.Version)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("recheck tagged source after build: %w", err)
|
||||
@@ -179,7 +187,7 @@ func validateBuild(repositoryDir, commit, version string, build *internalbuild.R
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(build.Targets) == 0 {
|
||||
if len(build.Settings.Targets) == 0 {
|
||||
if len(assets) != 0 {
|
||||
return errors.New("page-only delivery cannot include binary assets")
|
||||
}
|
||||
@@ -195,7 +203,7 @@ func validateBuild(repositoryDir, commit, version string, build *internalbuild.R
|
||||
if buildRepository != repositoryDir || build.CandidateCommit != commit || build.Version != version {
|
||||
return errors.New("build result does not identify the selected tagged source")
|
||||
}
|
||||
if strings.TrimSpace(build.MainPackage) == "" || strings.TrimSpace(build.BinaryName) == "" || len(assets) != len(build.Targets)+1 || assets[len(assets)-1].Filename != gitea.ManifestFilename {
|
||||
if strings.TrimSpace(build.Settings.MainPackage) == "" || strings.TrimSpace(build.Settings.BinaryName) == "" || len(assets) != len(build.Settings.Targets)+1 || assets[len(assets)-1].Filename != gitea.ManifestFilename {
|
||||
return errors.New("build result does not contain the selected artifacts and complete checksum manifest")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
||||
)
|
||||
@@ -60,14 +61,13 @@ func performanceBuildResult(b *testing.B) (internalbuild.Result, int64) {
|
||||
}
|
||||
digest := sha256.Sum256(data)
|
||||
result := internalbuild.Result{
|
||||
Version: "v1.2.3",
|
||||
BinaryName: "releasekit",
|
||||
Targets: []internalbuild.Target{
|
||||
Version: "v1.2.3",
|
||||
Settings: artifact.NormalizeSettings("./cmd/releasekit", "releasekit", []artifact.Target{
|
||||
{OS: "linux", Arch: "amd64"},
|
||||
{OS: "windows", Arch: "amd64"},
|
||||
},
|
||||
}),
|
||||
}
|
||||
for _, target := range result.Targets {
|
||||
for _, target := range result.Settings.Targets {
|
||||
name := fmt.Sprintf("releasekit-v1.2.3-%s-%s", target.OS, target.Arch)
|
||||
if target.OS == "windows" {
|
||||
name += ".exe"
|
||||
|
||||
@@ -8,32 +8,77 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Outside uses the system temporary directory and leaves cleanup to its caller.
|
||||
// Outside uses system temporary storage and falls back to the repository's
|
||||
// parent when TMPDIR resolves inside the worktree. Cleanup belongs to the caller.
|
||||
func Outside(repositoryDir, pattern string) (string, error) {
|
||||
repository, err := filepath.Abs(repositoryDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
repository, err = filepath.EvalSymlinks(repository)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
directory, err := os.MkdirTemp("", pattern)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
absolute, err := filepath.Abs(directory)
|
||||
created := directory
|
||||
directory, err = filepath.Abs(created)
|
||||
if err != nil {
|
||||
os.RemoveAll(created)
|
||||
return "", err
|
||||
}
|
||||
directory, err = filepath.EvalSymlinks(directory)
|
||||
if err != nil {
|
||||
os.RemoveAll(directory)
|
||||
return "", err
|
||||
}
|
||||
directory = absolute
|
||||
repository, err := filepath.Abs(repositoryDir)
|
||||
if err == nil && !strings.EqualFold(filepath.VolumeName(repository), filepath.VolumeName(directory)) {
|
||||
outside, err := isOutside(repository, directory)
|
||||
if err != nil {
|
||||
os.RemoveAll(directory)
|
||||
return "", err
|
||||
}
|
||||
if outside {
|
||||
return directory, nil
|
||||
}
|
||||
if err == nil {
|
||||
var relative string
|
||||
relative, err = filepath.Rel(repository, directory)
|
||||
if err == nil && (relative == "." || relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))) {
|
||||
err = fmt.Errorf("temporary storage %q is inside repository %q; select an external temporary directory", directory, repository)
|
||||
}
|
||||
if err := os.RemoveAll(directory); err != nil {
|
||||
return "", fmt.Errorf("remove temporary storage inside repository %q: %w", repository, err)
|
||||
}
|
||||
directory, err = os.MkdirTemp(filepath.Dir(repository), pattern)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create temporary storage outside repository %q: %w", repository, err)
|
||||
}
|
||||
created = directory
|
||||
directory, err = filepath.Abs(created)
|
||||
if err != nil {
|
||||
os.RemoveAll(created)
|
||||
return "", err
|
||||
}
|
||||
directory, err = filepath.EvalSymlinks(directory)
|
||||
if err != nil {
|
||||
os.RemoveAll(directory)
|
||||
return "", err
|
||||
}
|
||||
outside, err = isOutside(repository, directory)
|
||||
if err != nil {
|
||||
os.RemoveAll(directory)
|
||||
return "", err
|
||||
}
|
||||
if !outside {
|
||||
os.RemoveAll(directory)
|
||||
return "", fmt.Errorf("temporary storage %q is inside repository %q", directory, repository)
|
||||
}
|
||||
return directory, nil
|
||||
}
|
||||
|
||||
func isOutside(repository, directory string) (bool, error) {
|
||||
if !strings.EqualFold(filepath.VolumeName(repository), filepath.VolumeName(directory)) {
|
||||
return true, nil
|
||||
}
|
||||
relative, err := filepath.Rel(repository, directory)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)), nil
|
||||
}
|
||||
|
||||
@@ -29,16 +29,28 @@ func TestOutsideHonorsRelativeTemporaryDirectory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutsideRejectsAndCleansStorageInsideWorktree(t *testing.T) {
|
||||
repository := t.TempDir()
|
||||
setTemporaryDirectory(t, repository)
|
||||
if directory, err := Outside(repository, "fixture-"); err == nil {
|
||||
os.RemoveAll(directory)
|
||||
t.Fatal("storage created inside worktree")
|
||||
func TestOutsideFallsBackWhenTemporaryDirectoryIsInsideWorktree(t *testing.T) {
|
||||
parent := t.TempDir()
|
||||
repository := filepath.Join(parent, "worktree")
|
||||
if err := os.Mkdir(repository, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
localTemporary := filepath.Join(repository, "tmp")
|
||||
if err := os.Mkdir(localTemporary, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
setTemporaryDirectory(t, localTemporary)
|
||||
directory, err := Outside(repository, "fixture-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(directory)
|
||||
if filepath.Dir(directory) != parent {
|
||||
t.Fatalf("temporary storage=%q want external parent %q", directory, parent)
|
||||
}
|
||||
remaining, err := os.ReadDir(repository)
|
||||
if err != nil || len(remaining) != 0 {
|
||||
t.Fatalf("temporary storage left behind: %v %v", remaining, err)
|
||||
if err != nil || len(remaining) != 1 || remaining[0].Name() != "tmp" {
|
||||
t.Fatalf("temporary storage left behind in worktree: %v %v", remaining, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user