diff --git a/internal/client/client.go b/internal/client/client.go index 1940e5b9..e1363195 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -304,7 +304,7 @@ func execute(ctx context.Context, req *resty.Request, m, u string, h func(*resty type silentLogger struct{} // Compile-time verification that 'silentLogger' implements 'resty.Logger'. -var _ resty.Logger = silentLogger{} +var _ resty.Logger = (*silentLogger)(nil) func (l silentLogger) Debugf(string, ...interface{}) {} func (l silentLogger) Errorf(string, ...interface{}) {} diff --git a/pkg/download/download.go b/pkg/download/download.go index 6b70951e..cd1affef 100644 --- a/pkg/download/download.go +++ b/pkg/download/download.go @@ -5,12 +5,90 @@ import ( "fmt" "io/fs" "os" + "path/filepath" + "github.com/charmbracelet/log" "github.com/coffeebeats/gdenv/internal/client" "github.com/coffeebeats/gdenv/pkg/godot/artifact" + "github.com/coffeebeats/gdenv/pkg/godot/mirror" "github.com/coffeebeats/gdenv/pkg/progress" ) +type progressKey[T artifact.Versioned] struct{} + +/* -------------------------------------------------------------------------- */ +/* Function: WithProgress */ +/* -------------------------------------------------------------------------- */ + +// WithProgress creates a sub-context with an associated progress reporter. The +// result can be passed to download functions in this package to get updates on +// the download progress for that specific artifact. +func WithProgress[T artifact.Versioned]( + ctx context.Context, + p *progress.Progress, +) context.Context { + return context.WithValue(ctx, progressKey[T]{}, p) +} + +/* -------------------------------------------------------------------------- */ +/* Function: Download */ +/* -------------------------------------------------------------------------- */ + +// Download uses the provided mirror to download the specified artifact and +// returns an 'artifact.Local' wrapper pointing to it. +func Download[T artifact.Versioned]( + ctx context.Context, + a T, + out string, +) (artifact.Local[T], error) { + var local artifact.Local[T] + + if err := checkIsDirectory(out); err != nil { + return local, err + } + + m, err := mirror.Select(ctx, availableMirrors[T](), a) + if err != nil { + return local, err + } + + log.Infof("downloading '%s' from mirror: %s", a.Name(), m.Name()) + + remote, err := m.Remote(a) + if err != nil { + return local, err + } + + c := client.NewWithRedirectDomains(m.Hosts()...) + + out = filepath.Join(out, remote.Artifact.Name()) + + p, ok := ctx.Value(progressKey[T]{}).(*progress.Progress) + if ok && p != nil { + ctx = client.WithProgress(ctx, p) + } + + if err := c.DownloadTo(ctx, remote.URL, out); err != nil { + return local, err + } + + log.Debugf("downloaded artifact: %s", out) + + local.Artifact = remote.Artifact + local.Path = out + + return local, nil +} + +/* -------------------------------------------------------------------------- */ +/* Function: availableMirrors */ +/* -------------------------------------------------------------------------- */ + +// availableMirrors returns the list of possible 'Mirror' hosts. +func availableMirrors[T artifact.Versioned]() []mirror.Mirror[T] { + return []mirror.Mirror[T]{mirror.GitHub[T]{}, mirror.TuxFamily[T]{}} +} + /* -------------------------------------------------------------------------- */ /* Function: checkIsDirectory */ /* -------------------------------------------------------------------------- */ @@ -29,24 +107,3 @@ func checkIsDirectory(path string) error { return nil } - -/* -------------------------------------------------------------------------- */ -/* Function: downloadArtifact */ -/* -------------------------------------------------------------------------- */ - -// downloadArtifact downloads an artifact and reports progress to the progress -// reporter extracted from the context using the provided key. -func downloadArtifact[T artifact.Artifact]( - ctx context.Context, - c *client.Client, - a artifact.Remote[T], - out string, - progressKey any, -) error { - p, ok := ctx.Value(progressKey).(*progress.Progress) - if ok && p != nil { - ctx = client.WithProgress(ctx, p) - } - - return c.DownloadTo(ctx, a.URL, out) -} diff --git a/pkg/download/executable.go b/pkg/download/executable.go index 4521a4e5..e197f06a 100644 --- a/pkg/download/executable.go +++ b/pkg/download/executable.go @@ -2,57 +2,21 @@ package download import ( "context" - "fmt" - "path/filepath" - "github.com/charmbracelet/log" - "github.com/coffeebeats/gdenv/internal/client" "github.com/coffeebeats/gdenv/pkg/godot/artifact" "github.com/coffeebeats/gdenv/pkg/godot/artifact/checksum" "github.com/coffeebeats/gdenv/pkg/godot/artifact/executable" - "github.com/coffeebeats/gdenv/pkg/godot/mirror" - "github.com/coffeebeats/gdenv/pkg/godot/version" - "github.com/coffeebeats/gdenv/pkg/progress" "golang.org/x/sync/errgroup" ) -type ( - progressKeyExecutable struct{} - progressKeyExecutableChecksum struct{} - - localExArchive = artifact.Local[executable.Archive] - localExChecksums = artifact.Local[checksum.Executable] -) - -/* -------------------------------------------------------------------------- */ -/* Function: WithExecutableProgress */ -/* -------------------------------------------------------------------------- */ - -// WithSourceProgress creates a sub-context with an associated progress -// reporter. The result can be passed to download functions in this package to -// get updates on download progress. -func WithExecutableProgress(ctx context.Context, p *progress.Progress) context.Context { - return context.WithValue(ctx, progressKeyExecutable{}, p) -} - -/* -------------------------------------------------------------------------- */ -/* Function: WithExecutableChecksumProgress */ -/* -------------------------------------------------------------------------- */ - -// WithExecutableChecksumProgress creates a sub-context with an associated -// progress reporter. The result can be passed to download functions in this -// package to get updates on download progress. -func WithExecutableChecksumProgress(ctx context.Context, p *progress.Progress) context.Context { - return context.WithValue(ctx, progressKeyExecutableChecksum{}, p) -} - /* -------------------------------------------------------------------------- */ /* Function: ExecutableWithChecksumValidation */ /* -------------------------------------------------------------------------- */ +// ExecutableWithChecksumValidation downloads an executable archive and +// validates that its checksum matches the published value. func ExecutableWithChecksumValidation( ctx context.Context, - m mirror.Mirror, ex executable.Executable, out string, ) (artifact.Local[executable.Archive], error) { @@ -60,10 +24,12 @@ func ExecutableWithChecksumValidation( defer close(chArchive) defer close(chChecksums) - eg, downloadCtx := errgroup.WithContext(ctx) + eg, ctxDownload := errgroup.WithContext(ctx) eg.Go(func() error { - result, err := Executable(downloadCtx, m, ex, out) + exArchive := executable.Archive{Artifact: ex} + + result, err := Download(ctxDownload, exArchive, out) if err != nil { return err } @@ -78,7 +44,12 @@ func ExecutableWithChecksumValidation( }) eg.Go(func() error { - result, err := ExecutableChecksums(downloadCtx, m, ex.Version(), out) + checksums, err := checksum.NewExecutable(ex.Version()) + if err != nil { + return err + } + + result, err := Download(ctxDownload, checksums, out) if err != nil { return err } @@ -104,85 +75,3 @@ func ExecutableWithChecksumValidation( return exArchive, nil } - -/* -------------------------------------------------------------------------- */ -/* Function: Executable */ -/* -------------------------------------------------------------------------- */ - -// Executable downloads the Godot 'executable.Archive' for a specific version -// and platform and returns an 'artifact.Local' encapsulating the result. -func Executable( - ctx context.Context, - m mirror.Mirror, - ex executable.Executable, - out string, -) (localExArchive, error) { - if err := checkIsDirectory(out); err != nil { - return localExArchive{}, err - } - - executableMirror, ok := m.(mirror.Executable) - if !ok || executableMirror == nil { - return localExArchive{}, fmt.Errorf("%w: executables", mirror.ErrNotSupported) - } - - remote, err := executableMirror.ExecutableArchive(ex.Version(), ex.Platform()) - if err != nil { - return localExArchive{}, err - } - - c := client.NewWithRedirectDomains(m.Domains()...) - - out = filepath.Join(out, remote.Artifact.Name()) - if err := downloadArtifact(ctx, c, remote, out, progressKeyExecutable{}); err != nil { - return localExArchive{}, err - } - - log.Debugf("downloaded executable: %s", out) - - return localExArchive{ - Artifact: remote.Artifact, - Path: out, - }, nil -} - -/* -------------------------------------------------------------------------- */ -/* Function: ExecutableChecksums */ -/* -------------------------------------------------------------------------- */ - -// ExecutableChecksums downloads the Godot 'checksum.Source' file for a specific -// version and returns an 'artifact.Local' encapsulating the result. -func ExecutableChecksums( - ctx context.Context, - m mirror.Mirror, - v version.Version, - out string, -) (localExChecksums, error) { - if err := checkIsDirectory(out); err != nil { - return localExChecksums{}, err - } - - executableMirror, ok := m.(mirror.Executable) - if !ok || executableMirror == nil { - return localExChecksums{}, fmt.Errorf("%w: executables", mirror.ErrNotSupported) - } - - remote, err := executableMirror.ExecutableArchiveChecksums(v) - if err != nil { - return localExChecksums{}, err - } - - c := client.NewWithRedirectDomains(m.Domains()...) - - out = filepath.Join(out, remote.Artifact.Name()) - if err := downloadArtifact(ctx, c, remote, out, progressKeyExecutableChecksum{}); err != nil { - return localExChecksums{}, err - } - - log.Debugf("downloaded checksums file: %s", out) - - return localExChecksums{ - Artifact: remote.Artifact, - Path: out, - }, nil -} diff --git a/pkg/download/source.go b/pkg/download/source.go index 8e74a97f..167cf88b 100644 --- a/pkg/download/source.go +++ b/pkg/download/source.go @@ -2,57 +2,22 @@ package download import ( "context" - "fmt" - "path/filepath" - "github.com/charmbracelet/log" - "github.com/coffeebeats/gdenv/internal/client" "github.com/coffeebeats/gdenv/pkg/godot/artifact" "github.com/coffeebeats/gdenv/pkg/godot/artifact/checksum" "github.com/coffeebeats/gdenv/pkg/godot/artifact/source" - "github.com/coffeebeats/gdenv/pkg/godot/mirror" "github.com/coffeebeats/gdenv/pkg/godot/version" - "github.com/coffeebeats/gdenv/pkg/progress" "golang.org/x/sync/errgroup" ) -type ( - progressKeySource struct{} - progressKeySourceChecksum struct{} - - localSourceArchive = artifact.Local[source.Archive] - localSourceChecksums = artifact.Local[checksum.Source] -) - -/* -------------------------------------------------------------------------- */ -/* Function: WithProgress */ -/* -------------------------------------------------------------------------- */ - -// WithSourceProgress creates a sub-context with an associated progress -// reporter. The result can be passed to download functions in this package to -// get updates on download progress. -func WithSourceProgress(ctx context.Context, p *progress.Progress) context.Context { - return context.WithValue(ctx, progressKeySource{}, p) -} - -/* -------------------------------------------------------------------------- */ -/* Function: WithProgress */ -/* -------------------------------------------------------------------------- */ - -// WithSourceChecksumProgress creates a sub-context with an associated progress -// reporter. The result can be passed to download functions in this package to -// get updates on download progress. -func WithSourceChecksumProgress(ctx context.Context, p *progress.Progress) context.Context { - return context.WithValue(ctx, progressKeySourceChecksum{}, p) -} - /* -------------------------------------------------------------------------- */ /* Function: SourceWithChecksumValidation */ /* -------------------------------------------------------------------------- */ +// SourceWithChecksumValidation downloads a source code archive and validates +// that its checksum matches the published value. func SourceWithChecksumValidation( ctx context.Context, - m mirror.Mirror, v version.Version, out string, ) (artifact.Local[source.Archive], error) { @@ -63,7 +28,9 @@ func SourceWithChecksumValidation( eg, ctxDownload := errgroup.WithContext(ctx) eg.Go(func() error { - result, err := Source(ctxDownload, m, v, out) + srcArchive := source.Archive{Artifact: source.New(v)} + + result, err := Download(ctxDownload, srcArchive, out) if err != nil { return err } @@ -78,7 +45,12 @@ func SourceWithChecksumValidation( }) eg.Go(func() error { - result, err := SourceChecksums(ctxDownload, m, v, out) + checksums, err := checksum.NewSource(v) + if err != nil { + return err + } + + result, err := Download(ctxDownload, checksums, out) if err != nil { return err } @@ -95,94 +67,12 @@ func SourceWithChecksumValidation( sourceArchive, checksums := <-chSource, <-chChecksums if err := eg.Wait(); err != nil { - return localSourceArchive{}, err + return artifact.Local[source.Archive]{}, err } if err := checksum.Compare[source.Archive](ctx, sourceArchive, checksums); err != nil { - return localSourceArchive{}, err + return artifact.Local[source.Archive]{}, err } return sourceArchive, nil } - -/* -------------------------------------------------------------------------- */ -/* Function: Source */ -/* -------------------------------------------------------------------------- */ - -// Source downloads the Godot 'source.Archive' for a specific version and -// returns an 'artifact.Local' encapsulating the result. -func Source( - ctx context.Context, - m mirror.Mirror, - v version.Version, - out string, -) (localSourceArchive, error) { - if err := checkIsDirectory(out); err != nil { - return localSourceArchive{}, err - } - - sourceMirror, ok := m.(mirror.Source) - if !ok || sourceMirror == nil { - return localSourceArchive{}, fmt.Errorf("%w: source code", mirror.ErrNotSupported) - } - - remote, err := sourceMirror.SourceArchive(v) - if err != nil { - return localSourceArchive{}, err - } - - c := client.NewWithRedirectDomains(m.Domains()...) - - out = filepath.Join(out, remote.Artifact.Name()) - if err := downloadArtifact(ctx, c, remote, out, progressKeySource{}); err != nil { - return localSourceArchive{}, err - } - - log.Debugf("downloaded source: %s", out) - - return localSourceArchive{ - Artifact: remote.Artifact, - Path: out, - }, nil -} - -/* -------------------------------------------------------------------------- */ -/* Function: SourceChecksums */ -/* -------------------------------------------------------------------------- */ - -// SourceChecksums downloads the Godot 'checksum.Source' file for a specific -// version and returns an 'artifact.Local' encapsulating the result. -func SourceChecksums( - ctx context.Context, - m mirror.Mirror, - v version.Version, - out string, -) (localSourceChecksums, error) { - if err := checkIsDirectory(out); err != nil { - return localSourceChecksums{}, err - } - - sourceMirror, ok := m.(mirror.Source) - if !ok || sourceMirror == nil { - return localSourceChecksums{}, fmt.Errorf("%w: source code", mirror.ErrNotSupported) - } - - remote, err := sourceMirror.SourceArchiveChecksums(v) - if err != nil { - return localSourceChecksums{}, err - } - - c := client.NewWithRedirectDomains(m.Domains()...) - - out = filepath.Join(out, remote.Artifact.Name()) - if err := downloadArtifact(ctx, c, remote, out, progressKeySourceChecksum{}); err != nil { - return localSourceChecksums{}, err - } - - log.Debugf("downloaded checksums file: %s", out) - - return localSourceChecksums{ - Artifact: remote.Artifact, - Path: out, - }, nil -} diff --git a/pkg/godot/artifact/archive/archive.go b/pkg/godot/artifact/archive/archive.go index 1deb52ff..c352fd98 100644 --- a/pkg/godot/artifact/archive/archive.go +++ b/pkg/godot/artifact/archive/archive.go @@ -38,7 +38,7 @@ type Local = artifact.Local[Archive] // An interface representing a compressed 'Artifact' archive. type Archive interface { - artifact.Artifact + artifact.Versioned extract(ctx context.Context, path, out string) error } @@ -50,7 +50,7 @@ type Archive interface { // An interface representing an 'Artifact' that can be compressed into an // archive. type Archivable interface { - artifact.Artifact + artifact.Versioned Archivable() } diff --git a/pkg/godot/artifact/archive/archive_test.go b/pkg/godot/artifact/archive/archive_test.go index 2a5ff317..1a88b8b1 100644 --- a/pkg/godot/artifact/archive/archive_test.go +++ b/pkg/godot/artifact/archive/archive_test.go @@ -10,6 +10,7 @@ import ( "github.com/coffeebeats/gdenv/internal/osutil" "github.com/coffeebeats/gdenv/pkg/godot/artifact/artifacttest" + "github.com/coffeebeats/gdenv/pkg/godot/version" ) type MockArtifact = artifacttest.MockArtifact @@ -124,11 +125,12 @@ func TestExtract(t *testing.T) { /* -------------------------------------------------------------------------- */ type MockArchive[T Archivable] struct { - name string - err error + name string + version version.Version + err error } -var _ Archive = MockArchive[artifacttest.MockArtifact]{} +var _ Archive = (*MockArchive[artifacttest.MockArtifact])(nil) /* ----------------------------- Impl: Artifact ----------------------------- */ @@ -136,6 +138,12 @@ func (a MockArchive[T]) Name() string { return a.name } +/* ----------------------------- Impl: Versioned ---------------------------- */ + +func (a MockArchive[T]) Version() version.Version { + return a.version +} + /* ------------------------------ Impl: Archive ----------------------------- */ func (a MockArchive[T]) extract(_ context.Context, path, out string) error { diff --git a/pkg/godot/artifact/archive/tarxz.go b/pkg/godot/artifact/archive/tarxz.go index 7156c16f..591be0a7 100644 --- a/pkg/godot/artifact/archive/tarxz.go +++ b/pkg/godot/artifact/archive/tarxz.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/coffeebeats/gdenv/internal/osutil" + "github.com/coffeebeats/gdenv/pkg/godot/version" "github.com/coffeebeats/gdenv/pkg/progress" "github.com/ulikunitz/xz" ) @@ -37,6 +38,12 @@ func (a TarXZ[T]) Name() string { return name } +/* ----------------------------- Impl: Versioned ---------------------------- */ + +func (a TarXZ[T]) Version() version.Version { + return a.Artifact.Version() +} + /* ------------------------------ Impl: Archive ----------------------------- */ // Extracts the archived contents to the specified directory. diff --git a/pkg/godot/artifact/archive/tarxz_test.go b/pkg/godot/artifact/archive/tarxz_test.go index 0c7b85cb..d215047d 100644 --- a/pkg/godot/artifact/archive/tarxz_test.go +++ b/pkg/godot/artifact/archive/tarxz_test.go @@ -3,15 +3,57 @@ package archive import ( "context" "errors" + "fmt" "os" "path/filepath" + "reflect" "testing" "github.com/coffeebeats/gdenv/internal/fstest" "github.com/coffeebeats/gdenv/internal/osutil" + "github.com/coffeebeats/gdenv/pkg/godot/artifact/artifacttest" + "github.com/coffeebeats/gdenv/pkg/godot/version" "github.com/coffeebeats/gdenv/pkg/progress" ) +/* ----------------------- Function: TestTarXZVersion ----------------------- */ + +func TestTarXZVersion(t *testing.T) { + tests := []struct { + artifact Archivable + + want version.Version + }{ + { + artifact: artifacttest.MockArtifact{}, + want: version.Version{}, + }, + { + artifact: artifacttest.NewWithVersion(version.Godot3()), + want: version.Godot3(), + }, + { + artifact: artifacttest.NewWithVersion(version.Godot4()), + want: version.Godot4(), + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("%d-%s", i, tc.artifact.Version()), func(t *testing.T) { + // Given: An archive wrapping the specified artifact. + a := TarXZ[Archivable]{Artifact: tc.artifact} + + // When: The archive's version is determined. + got := a.Version() + + // Then: The version matches the artifact's version. + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("output: got %#v, want %#v", got, tc.want) + } + }) + } +} + /* ----------------------- Function: TestTarXZExtract ----------------------- */ func TestTarXZExtract(t *testing.T) { diff --git a/pkg/godot/artifact/archive/zip.go b/pkg/godot/artifact/archive/zip.go index 5808350a..bf2d38ca 100644 --- a/pkg/godot/artifact/archive/zip.go +++ b/pkg/godot/artifact/archive/zip.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/coffeebeats/gdenv/internal/osutil" + "github.com/coffeebeats/gdenv/pkg/godot/version" "github.com/coffeebeats/gdenv/pkg/progress" ) @@ -35,6 +36,12 @@ func (a Zip[T]) Name() string { return name } +/* ----------------------------- Impl: Versioned ---------------------------- */ + +func (a Zip[T]) Version() version.Version { + return a.Artifact.Version() +} + /* ------------------------------ Impl: Archive ----------------------------- */ // Extracts the archived contents to the specified directory. diff --git a/pkg/godot/artifact/archive/zip_test.go b/pkg/godot/artifact/archive/zip_test.go index 894bf826..1d0cdb32 100644 --- a/pkg/godot/artifact/archive/zip_test.go +++ b/pkg/godot/artifact/archive/zip_test.go @@ -3,15 +3,57 @@ package archive import ( "context" "errors" + "fmt" "os" "path/filepath" + "reflect" "testing" "github.com/coffeebeats/gdenv/internal/fstest" "github.com/coffeebeats/gdenv/internal/osutil" + "github.com/coffeebeats/gdenv/pkg/godot/artifact/artifacttest" + "github.com/coffeebeats/gdenv/pkg/godot/version" "github.com/coffeebeats/gdenv/pkg/progress" ) +/* ------------------------ Function: TestZipVersion ------------------------ */ + +func TestZipVersion(t *testing.T) { + tests := []struct { + artifact Archivable + + want version.Version + }{ + { + artifact: artifacttest.MockArtifact{}, + want: version.Version{}, + }, + { + artifact: artifacttest.NewWithVersion(version.Godot3()), + want: version.Godot3(), + }, + { + artifact: artifacttest.NewWithVersion(version.Godot4()), + want: version.Godot4(), + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("%d-%s", i, tc.artifact.Version()), func(t *testing.T) { + // Given: An archive wrapping the specified artifact. + a := Zip[Archivable]{Artifact: tc.artifact} + + // When: The archive's version is determined. + got := a.Version() + + // Then: The version matches the artifact's version. + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("output: got %#v, want %#v", got, tc.want) + } + }) + } +} + /* ------------------------ Function: TestZipExtract ------------------------ */ func TestZipExtract(t *testing.T) { diff --git a/pkg/godot/artifact/artifacttest/artifact.go b/pkg/godot/artifact/artifacttest/artifact.go index e41fd39a..7c6b7565 100644 --- a/pkg/godot/artifact/artifacttest/artifact.go +++ b/pkg/godot/artifact/artifacttest/artifact.go @@ -1,11 +1,21 @@ package artifacttest +import "github.com/coffeebeats/gdenv/pkg/godot/version" + /* -------------------------------------------------------------------------- */ /* Struct: MockArtifact */ /* -------------------------------------------------------------------------- */ type MockArtifact struct { - name string + name string + version version.Version +} + +/* ------------------------ Function: NewWithVersion ------------------------ */ + +// NewWithVersion creates a new mock artifact with the specified version. +func NewWithVersion(v version.Version) MockArtifact { + return MockArtifact{name: "", version: v} } /* ----------------------------- Impl: Artifact ----------------------------- */ @@ -14,6 +24,12 @@ func (a MockArtifact) Name() string { return a.name } +/* ----------------------------- Impl: Versioned ---------------------------- */ + +func (a MockArtifact) Version() version.Version { + return a.version +} + /* ---------------------------- Impl: Archivable ---------------------------- */ func (a MockArtifact) Archivable() {} diff --git a/pkg/godot/artifact/checksum/executable.go b/pkg/godot/artifact/checksum/executable.go index d2ebb257..1a63b62d 100644 --- a/pkg/godot/artifact/checksum/executable.go +++ b/pkg/godot/artifact/checksum/executable.go @@ -16,10 +16,10 @@ const filenameChecksums = "SHA512-SUMS.txt" type Executable checksums // Compile-time verifications that 'Executable' implements 'Artifact'. -var _ artifact.Artifact = Executable{} //nolint:exhaustruct +var _ artifact.Artifact = (*Executable)(nil) // Compile-time verifications that 'Executable' implements 'Checksums'. -var _ Checksums[executable.Archive] = Executable{} //nolint:exhaustruct +var _ Checksums[executable.Archive] = (*Executable)(nil) /* ------------------------- Function: NewExecutable ------------------------ */ diff --git a/pkg/godot/artifact/checksum/source.go b/pkg/godot/artifact/checksum/source.go index c58ab597..caad3596 100644 --- a/pkg/godot/artifact/checksum/source.go +++ b/pkg/godot/artifact/checksum/source.go @@ -15,10 +15,10 @@ import ( type Source checksums // Compile-time verifications that 'Executable' implements 'Artifact'. -var _ artifact.Artifact = Source{} //nolint:exhaustruct +var _ artifact.Artifact = (*Source)(nil) // Compile-time verifications that 'Source' implements 'Checksums'. -var _ Checksums[source.Archive] = Source{} //nolint:exhaustruct +var _ Checksums[source.Archive] = (*Source)(nil) /* --------------------------- Function: NewSource -------------------------- */ diff --git a/pkg/godot/artifact/executable/executable.go b/pkg/godot/artifact/executable/executable.go index 58436b09..cbba9210 100644 --- a/pkg/godot/artifact/executable/executable.go +++ b/pkg/godot/artifact/executable/executable.go @@ -33,7 +33,7 @@ type Executable struct { } // Compile-time verifications that 'Executable' implements 'Artifact'. -var _ artifact.Artifact = Executable{} //nolint:exhaustruct +var _ artifact.Artifact = (*Executable)(nil) /* ------------------------------ Function: New ----------------------------- */ diff --git a/pkg/godot/artifact/source/source.go b/pkg/godot/artifact/source/source.go index 17f715e6..5b676520 100644 --- a/pkg/godot/artifact/source/source.go +++ b/pkg/godot/artifact/source/source.go @@ -28,7 +28,7 @@ type Source struct { } // Compile-time verifications that 'Source' implements 'Artifact'. -var _ artifact.Artifact = Source{} //nolint:exhaustruct +var _ artifact.Artifact = (*Source)(nil) /* ------------------------------ Function: New ----------------------------- */ diff --git a/pkg/godot/mirror/github.go b/pkg/godot/mirror/github.go index 0ca83ad9..3049e095 100644 --- a/pkg/godot/mirror/github.go +++ b/pkg/godot/mirror/github.go @@ -10,157 +10,70 @@ import ( "github.com/coffeebeats/gdenv/pkg/godot/artifact/checksum" "github.com/coffeebeats/gdenv/pkg/godot/artifact/executable" "github.com/coffeebeats/gdenv/pkg/godot/artifact/source" - "github.com/coffeebeats/gdenv/pkg/godot/platform" "github.com/coffeebeats/gdenv/pkg/godot/version" ) const ( gitHubContentDomain = "objects.githubusercontent.com" - gitHubAssetsURLBase = "https://github.com/godotengine/godot/releases/download" + gitHubAssetsURLBase = "https://github.com/godotengine/godot-builds/releases/download" ) -var versionGitHubAssetSupport = version.MustParse("v3.1.1") //nolint:gochecknoglobals - /* -------------------------------------------------------------------------- */ /* Struct: GitHub */ /* -------------------------------------------------------------------------- */ // A mirror implementation for fetching artifacts via releases on the Godot // GitHub repository. -type GitHub struct{} +type GitHub[T artifact.Versioned] struct{} // Validate at compile-time that 'GitHub' implements 'Mirror' interfaces. -var _ Mirror = &GitHub{} -var _ Executable = &GitHub{} -var _ Source = &GitHub{} +var _ Hoster = (*GitHub[artifact.Versioned])(nil) +var _ Remoter[artifact.Versioned] = (*GitHub[artifact.Versioned])(nil) -/* ------------------------------ Impl: Mirror ------------------------------ */ +/* ------------------------------ Impl: Hoster ------------------------------ */ -// Returns a new 'client.Client' for downloading artifacts from the mirror. -func (m GitHub) Domains() []string { +// Hosts returns the host URLs at which artifacts are hosted. +func (m GitHub[T]) Hosts() []string { return []string{gitHubContentDomain} } -// Checks whether the version is broadly supported by the mirror. No network -// request is issued, but this does not guarantee the host has the version. -// To check whether the host has the version definitively via the network, -// use the 'checkIfExists' method. -func (m GitHub) Supports(v version.Version) bool { - // GitHub only contains stable releases, starting with 'versionGitHubAssetSupport'. - return v.IsStable() && v.CompareNormal(versionGitHubAssetSupport) >= 0 -} - -/* ---------------------------- Impl: Executable ---------------------------- */ - -func (m GitHub) ExecutableArchive(v version.Version, p platform.Platform) (artifact.Remote[executable.Archive], error) { - var a artifact.Remote[executable.Archive] - - if !m.Supports(v) { - return a, fmt.Errorf("%w: '%s'", ErrInvalidSpecification, v) - } - - urlRelease, err := urlGitHubRelease(v) - if err != nil { - return a, errors.Join(ErrInvalidURL, err) - } - - executableArchive := executable.Archive{Artifact: executable.New(v, p)} - - urlParsed, err := client.ParseURL(urlRelease, executableArchive.Name()) - if err != nil { - return a, errors.Join(ErrInvalidURL, err) - } +/* ------------------------------ Impl: Remoter ----------------------------- */ - a.Artifact, a.URL = executableArchive, urlParsed +// Remote returns an 'artifact.Remote' wrapper around a specified artifact. The +// remote wrapper contains the URL at which the artifact can be downloaded. +func (m GitHub[T]) Remote(a T) (artifact.Remote[T], error) { + var remote artifact.Remote[T] - return a, nil -} - -func (m GitHub) ExecutableArchiveChecksums(v version.Version) (artifact.Remote[checksum.Executable], error) { - var a artifact.Remote[checksum.Executable] - - if !m.Supports(v) { - return a, fmt.Errorf("%w: '%s'", ErrInvalidSpecification, v.String()) - } - - urlRelease, err := urlGitHubRelease(v) - if err != nil { - return a, errors.Join(ErrInvalidURL, err) - } - - checksumsExecutable, err := checksum.NewExecutable(v) - if err != nil { - return a, errors.Join(ErrInvalidSpecification, err) - } - - urlParsed, err := client.ParseURL(urlRelease, checksumsExecutable.Name()) - if err != nil { - return a, errors.Join(ErrInvalidURL, err) - } - - a.Artifact, a.URL = checksumsExecutable, urlParsed - - return a, nil -} - -/* ------------------------------ Impl: Source ------------------------------ */ - -func (m GitHub) SourceArchive(v version.Version) (artifact.Remote[source.Archive], error) { - var a artifact.Remote[source.Archive] - - if !m.Supports(v) { - return a, fmt.Errorf("%w: '%s'", ErrInvalidSpecification, v) - } - - urlRelease, err := urlGitHubRelease(v) - if err != nil { - return a, errors.Join(ErrInvalidURL, err) + switch any(a).(type) { // FIXME: https://github.com/golang/go/issues/45380 + case executable.Archive, source.Archive: + case checksum.Executable, checksum.Source: + default: + return remote, fmt.Errorf("%w: %T", ErrUnsupportedArtifact, a) } - s := source.New(v) - sourceArchive := source.Archive{Artifact: s} + urlRelease := urlGitHubRelease(a.Version()) - urlParsed, err := client.ParseURL(urlRelease, sourceArchive.Name()) + urlParsed, err := client.ParseURL(urlRelease, a.Name()) if err != nil { - return a, errors.Join(ErrInvalidURL, err) + return remote, errors.Join(ErrInvalidURL, err) } - a.Artifact, a.URL = sourceArchive, urlParsed + remote.Artifact, remote.URL = a, urlParsed - return a, nil + return remote, nil } -func (m GitHub) SourceArchiveChecksums(v version.Version) (artifact.Remote[checksum.Source], error) { - var a artifact.Remote[checksum.Source] - - if !m.Supports(v) { - return a, fmt.Errorf("%w: '%s'", ErrInvalidSpecification, v.String()) - } - - urlRelease, err := urlGitHubRelease(v) - if err != nil { - return a, errors.Join(ErrInvalidURL, err) - } - - checksumsSource, err := checksum.NewSource(v) - if err != nil { - return a, errors.Join(ErrInvalidSpecification, err) - } - - urlParsed, err := client.ParseURL(urlRelease, checksumsSource.Name()) - if err != nil { - return a, errors.Join(ErrInvalidURL, err) - } - - a.Artifact, a.URL = checksumsSource, urlParsed +/* ------------------------------ Impl: Mirror ------------------------------ */ - return a, nil +// Name returns the display name of the mirror. +func (m GitHub[T]) Name() string { + return "GitHub (github.com/godotengine/godot-builds)" } /* ----------------------- Function: urlGitHubRelease ----------------------- */ // Returns a URL to the version-specific release containing release assets. -func urlGitHubRelease(v version.Version) (string, error) { +func urlGitHubRelease(v version.Version) string { // The release will be tagged as the "normal version", but a patch version // of '0' will be dropped. var normal string @@ -174,5 +87,10 @@ func urlGitHubRelease(v version.Version) (string, error) { tag := fmt.Sprintf("%s-%s", normal, version.LabelStable) - return url.JoinPath(gitHubAssetsURLBase, tag) + releaseURL, err := url.JoinPath(gitHubAssetsURLBase, tag) + if err != nil { + panic(err) // This indicates an error in the asset URL base constant. + } + + return releaseURL } diff --git a/pkg/godot/mirror/github_test.go b/pkg/godot/mirror/github_test.go index e108710a..3f2f33f6 100644 --- a/pkg/godot/mirror/github_test.go +++ b/pkg/godot/mirror/github_test.go @@ -7,182 +7,71 @@ import ( "testing" "github.com/coffeebeats/gdenv/pkg/godot/artifact" - "github.com/coffeebeats/gdenv/pkg/godot/artifact/checksum" + "github.com/coffeebeats/gdenv/pkg/godot/artifact/artifacttest" "github.com/coffeebeats/gdenv/pkg/godot/artifact/executable" "github.com/coffeebeats/gdenv/pkg/godot/artifact/source" "github.com/coffeebeats/gdenv/pkg/godot/version" ) -/* --------------------- Test: GitHub.ExecutableArchive --------------------- */ +/* --------------------------- Test: GitHub.Remote -------------------------- */ -func TestGitHubExecutableArchive(t *testing.T) { +func TestGitHubRemote(t *testing.T) { tests := []struct { - ex executable.Executable - name string + artifact artifact.Versioned url *url.URL err error }{ // Invalid inputs - {ex: executable.Executable{}, err: ErrInvalidSpecification}, - {ex: executable.MustParse("Godot_v0.1.0-stable_linux.x86_64"), err: ErrInvalidSpecification}, - {ex: executable.MustParse("Godot_v4.1.1-unsupported-label_linux.x86_64"), err: ErrInvalidSpecification}, + {artifact: artifacttest.MockArtifact{}, err: ErrUnsupportedArtifact}, // Valid inputs { - ex: executable.MustParse("Godot_v4.1.1-stable_linux.x86_64"), - name: "Godot_v4.1.1-stable_linux.x86_64.zip", - url: mustParseURL(t, "https://github.com/godotengine/godot/releases/download/4.1.1-stable/Godot_v4.1.1-stable_linux.x86_64.zip"), + artifact: executable.Archive{Artifact: executable.MustParse("Godot_v4.1.1-stable_linux.x86_64")}, + url: mustParseURL(t, gitHubAssetsURLBase+"/4.1.1-stable/Godot_v4.1.1-stable_linux.x86_64.zip"), }, { - ex: executable.MustParse("Godot_v4.1-stable_linux.x86_64"), - name: "Godot_v4.1-stable_linux.x86_64.zip", - url: mustParseURL(t, "https://github.com/godotengine/godot/releases/download/4.1-stable/Godot_v4.1-stable_linux.x86_64.zip"), + artifact: executable.Archive{Artifact: executable.MustParse("Godot_v4.1-stable_linux.x86_64")}, + url: mustParseURL(t, gitHubAssetsURLBase+"/4.1-stable/Godot_v4.1-stable_linux.x86_64.zip"), }, - } - - for _, tc := range tests { - t.Run(tc.ex.String(), func(t *testing.T) { - got, err := (&GitHub{}).ExecutableArchive(tc.ex.Version(), tc.ex.Platform()) - - if !errors.Is(err, tc.err) { - t.Errorf("err: got %v, want %v", err, tc.err) - } - - if got := got.Artifact.Name(); got != tc.name { - t.Errorf("output: got %v, want %v", got, tc.name) - } - if got := got.URL; !reflect.DeepEqual(got, tc.url) { - t.Errorf("output: got %v, want %v", got, tc.url) - } - }) - } -} - -/* ----------------- Test: GitHub.ExecutableArchiveChecksums ---------------- */ - -func TestGitHubExecutableArchiveChecksums(t *testing.T) { - tests := []struct { - v version.Version - url *url.URL - err error - }{ - // Invalid inputs - {v: version.Version{}, err: ErrInvalidSpecification}, - {v: version.MustParse("v0.0.0"), err: ErrInvalidSpecification}, - {v: version.MustParse("v4.1.1-unsupported-label"), err: ErrInvalidSpecification}, - - // Valid inputs { - v: version.MustParse("4.1.1-stable"), - url: mustParseURL(t, "https://github.com/godotengine/godot/releases/download/4.1.1-stable/SHA512-SUMS.txt"), + artifact: source.Archive{Artifact: source.New(version.MustParse("4.1.1-stable"))}, + url: mustParseURL(t, gitHubAssetsURLBase+"/4.1.1-stable/godot-4.1.1-stable.tar.xz"), }, { - v: version.MustParse("4.1.0-stable"), - url: mustParseURL(t, "https://github.com/godotengine/godot/releases/download/4.1-stable/SHA512-SUMS.txt"), + artifact: source.Archive{Artifact: source.New(version.MustParse("4.1.0-stable"))}, + url: mustParseURL(t, gitHubAssetsURLBase+"/4.1-stable/godot-4.1-stable.tar.xz"), }, - } - - for _, tc := range tests { - t.Run(tc.v.String(), func(t *testing.T) { - got, err := (&GitHub{}).ExecutableArchiveChecksums(tc.v) - - if !errors.Is(err, tc.err) { - t.Errorf("err: got %#v, want %#v", err, tc.err) - } - - // The test setup below will fail for invalid inputs. - if tc.url == nil { - return - } - - ex, err := checksum.NewExecutable(tc.v) - if err != nil { - t.Fatalf("test setup: %v", err) - } - - want := artifact.Remote[checksum.Executable]{Artifact: ex, URL: tc.url} - if !reflect.DeepEqual(got, want) { - t.Errorf("output: got %#v, want %#v", got, want) - } - }) - } -} - -/* ----------------------- Test: GitHub.SourceArchive ----------------------- */ - -func TestGitHubSourceArchive(t *testing.T) { - tests := []struct { - v version.Version - - artifact source.Archive - url *url.URL - err error - }{ - // Invalid inputs - {v: version.Version{}, err: ErrInvalidSpecification}, - {v: version.MustParse("v0.1.0"), err: ErrInvalidSpecification}, - - // Valid inputs { - v: version.MustParse("v4.1.1"), - artifact: source.Archive{Artifact: source.New(version.MustParse("v4.1.1"))}, - url: mustParseURL(t, "https://github.com/godotengine/godot/releases/download/4.1.1-stable/godot-4.1.1-stable.tar.xz"), + artifact: mustMakeNewExecutableChecksum(t, version.MustParse("4.1.1-stable")), + url: mustParseURL(t, gitHubAssetsURLBase+"/4.1.1-stable/SHA512-SUMS.txt"), }, - } - - for _, tc := range tests { - t.Run(tc.v.String(), func(t *testing.T) { - remote, err := (&GitHub{}).SourceArchive(tc.v) - - if !errors.Is(err, tc.err) { - t.Errorf("err: got %v, want %v", err, tc.err) - } - - if got := remote.Artifact; !reflect.DeepEqual(got, tc.artifact) { - t.Errorf("output: got %v, want %v", got, tc.artifact) - } - if got := remote.URL; !reflect.DeepEqual(got, tc.url) { - t.Errorf("output: got %v, want %v", got, tc.url) - } - }) - } -} - -/* ------------------- Test: GitHub.SourceArchiveChecksums ------------------ */ - -func TestGitHubSourceArchiveChecksums(t *testing.T) { - tests := []struct { - v version.Version - - artifact checksum.Source - url *url.URL - err error - }{ - // Invalid inputs - {v: version.Version{}, err: ErrInvalidSpecification}, - {v: version.MustParse("v0.1.0"), err: ErrInvalidSpecification}, - - // Valid inputs { - v: version.MustParse("v4.1.1"), - artifact: mustMakeNewSource(t, version.MustParse("v4.1.1")), - url: mustParseURL(t, "https://github.com/godotengine/godot/releases/download/4.1.1-stable/godot-4.1.1-stable.tar.xz.sha256"), + artifact: mustMakeNewExecutableChecksum(t, version.MustParse("4.1.0-stable")), + url: mustParseURL(t, gitHubAssetsURLBase+"/4.1-stable/SHA512-SUMS.txt"), + }, + { + artifact: mustMakeNewSourceChecksum(t, version.MustParse("4.1.1-stable")), + url: mustParseURL(t, gitHubAssetsURLBase+"/4.1.1-stable/godot-4.1.1-stable.tar.xz.sha256"), + }, + { + artifact: mustMakeNewSourceChecksum(t, version.MustParse("4.1.0-stable")), + url: mustParseURL(t, gitHubAssetsURLBase+"/4.1-stable/godot-4.1-stable.tar.xz.sha256"), }, } for _, tc := range tests { - t.Run(tc.v.String(), func(t *testing.T) { - remote, err := (&GitHub{}).SourceArchiveChecksums(tc.v) + t.Run(tc.artifact.Name(), func(t *testing.T) { + got, err := (&GitHub[artifact.Versioned]{}).Remote(tc.artifact) if !errors.Is(err, tc.err) { t.Errorf("err: got %v, want %v", err, tc.err) } - if got := remote.Artifact; !reflect.DeepEqual(got, tc.artifact) { - t.Errorf("output: got %v, want %v", got, tc.artifact) + if got := got.Artifact; got != nil && got.Name() != tc.artifact.Name() { + t.Errorf("output: got %v, want %v", got, tc.artifact.Name()) } - if got := remote.URL; !reflect.DeepEqual(got, tc.url) { + if got := got.URL; !reflect.DeepEqual(got, tc.url) { t.Errorf("output: got %v, want %v", got, tc.url) } }) diff --git a/pkg/godot/mirror/mirror.go b/pkg/godot/mirror/mirror.go index d537b85f..7e91c0fd 100644 --- a/pkg/godot/mirror/mirror.go +++ b/pkg/godot/mirror/mirror.go @@ -7,20 +7,14 @@ import ( "github.com/coffeebeats/gdenv/internal/client" "github.com/coffeebeats/gdenv/pkg/godot/artifact" - "github.com/coffeebeats/gdenv/pkg/godot/artifact/checksum" - "github.com/coffeebeats/gdenv/pkg/godot/artifact/executable" - "github.com/coffeebeats/gdenv/pkg/godot/artifact/source" - "github.com/coffeebeats/gdenv/pkg/godot/platform" - "github.com/coffeebeats/gdenv/pkg/godot/version" "golang.org/x/sync/errgroup" ) var ( - ErrInvalidSpecification = errors.New("invalid specification") - ErrInvalidURL = errors.New("invalid URL") - ErrMissingMirrors = errors.New("no mirrors provided") - ErrNotFound = errors.New("no mirror found") - ErrNotSupported = errors.New("mirror not supported") + ErrInvalidURL = errors.New("invalid URL") + ErrMissingMirrors = errors.New("no mirrors provided") + ErrNotFound = errors.New("no mirror found") + ErrUnsupportedArtifact = errors.New("unsupported artifact") ) // clientKey is a context key used internally to replace the REST client used. @@ -30,59 +24,47 @@ type clientKey struct{} /* Interface: Mirror */ /* -------------------------------------------------------------------------- */ -// Specifies a host of Godot release artifacts. The associated methods are -// related to the host itself and not about individual artifacts. -type Mirror interface { - // Domains returns a slice of domains at which the mirror hosts artifacts. - Domains() []string - - // Checks whether the version is broadly supported by the mirror. No network - // request is issued, but this does not guarantee the host has the version. - // To check whether the host has the version definitively via the network, - // use the 'Has' method. - Supports(v version.Version) bool +// Mirror specifies a host of Godot release artifacts. +type Mirror[T artifact.Versioned] interface { + Hoster + Remoter[T] + + Name() string } /* -------------------------------------------------------------------------- */ -/* Interface: Executable */ +/* Interface: Hoster */ /* -------------------------------------------------------------------------- */ -// Executable is a mirror which hosts Godot executable artifacts. This does not -// imply that *all* executable versions are hosted, so users should be prepared -// to handle the case where resolving the artifact URL fails. -type Executable interface { - Mirror - - ExecutableArchive(v version.Version, p platform.Platform) (artifact.Remote[executable.Archive], error) - ExecutableArchiveChecksums(v version.Version) (artifact.Remote[checksum.Executable], error) +// Hoster is a mirror which describes the host URLs at which it hosts content. +// This can be used to restrict redirects when downloading artifacts, improving +// security. +type Hoster interface { + // Hosts returns a slice of URL hosts at which the mirror hosts artifacts. + Hosts() []string } /* -------------------------------------------------------------------------- */ -/* Interface: Source */ +/* Interface: Remoter */ /* -------------------------------------------------------------------------- */ -// Source is a mirror which hosts Godot repository source code versions. This -// does not imply that *all* executable versions are hosted, so users should be -// prepared to handle the case where resolving the artifact URL fails. -type Source interface { - Mirror - - SourceArchive(v version.Version) (artifact.Remote[source.Archive], error) - SourceArchiveChecksums(v version.Version) (artifact.Remote[checksum.Source], error) +// Remoter is a type that can resolve the URL at which a specified artifact is +// hosted. Provided artifacts must be versioned. +type Remoter[T artifact.Artifact] interface { + Remote(a T) (artifact.Remote[T], error) } /* -------------------------------------------------------------------------- */ /* Function: Select */ /* -------------------------------------------------------------------------- */ -// Select chooses the best 'Mirror' of those provided for downloading assets -// corresponding to the specified version and platform of Godot. -func Select( //nolint:ireturn +// Select chooses the best 'Mirror' of those provided for downloading the +// specified Godot release artifact. +func Select[T artifact.Versioned]( ctx context.Context, - v version.Version, - p platform.Platform, - mirrors []Mirror, -) (Mirror, error) { + mirrors []Mirror[T], + a T, +) (Mirror[T], error) { if len(mirrors) == 0 { return nil, ErrMissingMirrors } @@ -97,16 +79,13 @@ func Select( //nolint:ireturn eg.Wait() //nolint:errcheck }() - selected := make(chan Mirror) + selected := make(chan Mirror[T]) for _, m := range mirrors { - executableMirror, ok := m.(Executable) - if !ok || executableMirror == nil { - continue - } + m := m // Prevent capture of loop variable. eg.Go(func() error { - ok, err := checkIfExists(ctx, executableMirror, v, p) + ok, err := checkIfExists(ctx, m, a) if err != nil { return err } @@ -116,7 +95,7 @@ func Select( //nolint:ireturn } select { - case selected <- executableMirror: + case selected <- m: case <-ctx.Done(): return ctx.Err() } @@ -141,17 +120,12 @@ func Select( //nolint:ireturn /* ------------------------- Function: checkIfExists ------------------------ */ // Issues a request to the mirror host to determine if the artifact exists. -func checkIfExists( +func checkIfExists[T artifact.Versioned]( ctx context.Context, - m Executable, - v version.Version, - p platform.Platform, + m Mirror[T], + a T, ) (bool, error) { - if !m.Supports(v) { - return false, nil - } - - remote, err := m.ExecutableArchive(v, p) + remote, err := m.Remote(a) if err != nil { return false, err } @@ -162,7 +136,7 @@ func checkIfExists( // type. For now, this simply allows tests to inject a client. c, ok := ctx.Value(clientKey{}).(*client.Client) if !ok || c == nil { - c = client.NewWithRedirectDomains(m.Domains()...) + c = client.NewWithRedirectDomains(m.Hosts()...) } exists, err := c.Exists(ctx, remote.URL.String()) @@ -178,8 +152,11 @@ func checkIfExists( // chooseBest selects the best mirror from those available. The lowest indexed // 'Mirror' in 'ranking' will be returned. If none are available an error is // returned. -func chooseBest(available <-chan Mirror, ranking []Mirror) (Mirror, error) { //nolint:ireturn - out, index := Mirror(nil), len(ranking) +func chooseBest[T artifact.Versioned]( + available <-chan Mirror[T], + ranking []Mirror[T], +) (Mirror[T], error) { + out, index := Mirror[T](nil), len(ranking) for m := range available { // Rank mirrors according to order in 'mirrors'. diff --git a/pkg/godot/mirror/mirror_test.go b/pkg/godot/mirror/mirror_test.go index aff67080..e3296c12 100644 --- a/pkg/godot/mirror/mirror_test.go +++ b/pkg/godot/mirror/mirror_test.go @@ -8,6 +8,7 @@ import ( "github.com/coffeebeats/gdenv/internal/client" "github.com/coffeebeats/gdenv/pkg/godot/artifact/checksum" + "github.com/coffeebeats/gdenv/pkg/godot/artifact/executable" "github.com/coffeebeats/gdenv/pkg/godot/platform" "github.com/coffeebeats/gdenv/pkg/godot/version" "github.com/go-resty/resty/v2" @@ -19,12 +20,12 @@ import ( func TestSelect(t *testing.T) { tests := []struct { name string - mirrors []Mirror + mirrors []Mirror[executable.Archive] v version.Version p platform.Platform expects map[string]httpmock.Responder - want Mirror + want Mirror[executable.Archive] err error }{ // Invalid inputs @@ -35,12 +36,12 @@ func TestSelect(t *testing.T) { }, { name: "no mirror supports version", - mirrors: []Mirror{TuxFamily{}, GitHub{}}, + mirrors: []Mirror[executable.Archive]{TuxFamily[executable.Archive]{}, GitHub[executable.Archive]{}}, v: version.Godot4(), p: platform.MustParse("win64"), expects: map[string]httpmock.Responder{ - "https://github.com/godotengine/godot/releases/download/4.0-stable/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(400, nil), - "https://downloads.tuxfamily.org/godotengine/4.0/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(400, nil), + "https://github.com/godotengine/godot-builds/releases/download/4.0-stable/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(400, nil), + "https://downloads.tuxfamily.org/godotengine/4.0/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(400, nil), }, err: ErrNotFound, @@ -49,38 +50,38 @@ func TestSelect(t *testing.T) { // Valid inputs { name: "one valid mirror is selected", - mirrors: []Mirror{GitHub{}}, + mirrors: []Mirror[executable.Archive]{GitHub[executable.Archive]{}}, v: version.Godot4(), p: platform.MustParse("win64"), expects: map[string]httpmock.Responder{ - "https://github.com/godotengine/godot/releases/download/4.0-stable/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(200, nil), + "https://github.com/godotengine/godot-builds/releases/download/4.0-stable/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(200, nil), }, - want: GitHub{}, + want: GitHub[executable.Archive]{}, }, { name: "best mirror is selected", - mirrors: []Mirror{TuxFamily{}, GitHub{}}, + mirrors: []Mirror[executable.Archive]{TuxFamily[executable.Archive]{}, GitHub[executable.Archive]{}}, v: version.Godot4(), p: platform.MustParse("win64"), expects: map[string]httpmock.Responder{ - "https://github.com/godotengine/godot/releases/download/4.0-stable/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(200, nil), - "https://downloads.tuxfamily.org/godotengine/4.0/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(200, nil), + "https://github.com/godotengine/godot-builds/releases/download/4.0-stable/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(200, nil), + "https://downloads.tuxfamily.org/godotengine/4.0/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(200, nil), }, - want: TuxFamily{}, // Appears first in 'mirrors'. + want: TuxFamily[executable.Archive]{}, // Appears first in 'mirrors'. }, { name: "worse mirror is selected if best isn't available", - mirrors: []Mirror{GitHub{}, TuxFamily{}}, + mirrors: []Mirror[executable.Archive]{GitHub[executable.Archive]{}, TuxFamily[executable.Archive]{}}, v: version.Godot4(), p: platform.MustParse("win64"), expects: map[string]httpmock.Responder{ - "https://github.com/godotengine/godot/releases/download/4.0-stable/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(400, nil), - "https://downloads.tuxfamily.org/godotengine/4.0/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(200, nil), + "https://github.com/godotengine/godot-builds/releases/download/4.0-stable/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(400, nil), + "https://downloads.tuxfamily.org/godotengine/4.0/Godot_v4.0-stable_win64.exe.zip": httpmock.NewBytesResponder(200, nil), }, - want: TuxFamily{}, // Only mirror with successful response. + want: TuxFamily[executable.Archive]{}, // Only mirror with successful response. }, } @@ -104,7 +105,7 @@ func TestSelect(t *testing.T) { ctx := context.WithValue(context.Background(), clientKey{}, c) // When: A 'Mirror' is selected from the list of options. - got, err := Select(ctx, tc.v, tc.p, tc.mirrors) + got, err := Select(ctx, tc.mirrors, executable.Archive{Artifact: executable.New(tc.v, tc.p)}) // Then: The resulting error matches expectations. if !errors.Is(err, tc.err) { @@ -119,17 +120,26 @@ func TestSelect(t *testing.T) { } } -/* ----------------------- Function: mustMakeNewSource ---------------------- */ +/* ----------------- Function: mustMakeNewExecutableChecksum ---------------- */ -func mustMakeNewSource(t *testing.T, v version.Version) checksum.Source { - t.Helper() +func mustMakeNewExecutableChecksum(t *testing.T, v version.Version) checksum.Executable { + c, err := checksum.NewExecutable(v) + if err != nil { + t.Fatalf("test setup: %v", err) + } + + return c +} + +/* ------------------- Function: mustMakeNewSourceChecksum ------------------ */ - s, err := checksum.NewSource(v) +func mustMakeNewSourceChecksum(t *testing.T, v version.Version) checksum.Source { + c, err := checksum.NewSource(v) if err != nil { - t.Fatalf("test setup: %#v", err) + t.Fatalf("test setup: %v", err) } - return s + return c } /* ------------------------- Function: mustParseURL ------------------------- */ diff --git a/pkg/godot/mirror/tuxfamily.go b/pkg/godot/mirror/tuxfamily.go index ec95ae75..e676057d 100644 --- a/pkg/godot/mirror/tuxfamily.go +++ b/pkg/godot/mirror/tuxfamily.go @@ -12,7 +12,6 @@ import ( "github.com/coffeebeats/gdenv/pkg/godot/artifact/checksum" "github.com/coffeebeats/gdenv/pkg/godot/artifact/executable" "github.com/coffeebeats/gdenv/pkg/godot/artifact/source" - "github.com/coffeebeats/gdenv/pkg/godot/platform" "github.com/coffeebeats/gdenv/pkg/godot/version" ) @@ -24,8 +23,6 @@ const ( ) var ( - versionTuxFamilyMinSupported = version.MustParse("v1.1") //nolint:gochecknoglobals - // This expression matches all Godot v4.0 "pre-alpha" versions which use a // release label similar to 'dev.20211015'. This expressions has been tested // manually. @@ -37,137 +34,50 @@ var ( /* -------------------------------------------------------------------------- */ // A mirror implementation for fetching artifacts via the Godot TuxFamily host. -type TuxFamily struct{} +type TuxFamily[T artifact.Versioned] struct{} // Validate at compile-time that 'TuxFamily' implements 'Mirror' interfaces. -var _ Mirror = &TuxFamily{} -var _ Executable = &TuxFamily{} -var _ Source = &TuxFamily{} - -/* ------------------------------ Impl: Mirror ------------------------------ */ - -// Returns a new 'client.Client' for downloading artifacts from the mirror. -func (m TuxFamily) Domains() []string { - return nil -} - -// Checks whether the version is broadly supported by the mirror. No network -// request is issued, but this does not guarantee the host has the version. -// To check whether the host has the version definitively via the network, -// use the 'checkIfExists' method. -func (m TuxFamily) Supports(v version.Version) bool { - // TuxFamily seems to contain all published releases. - return v.CompareNormal(versionTuxFamilyMinSupported) >= 0 -} - -/* ---------------------------- Impl: Executable ---------------------------- */ - -func (m TuxFamily) ExecutableArchive( - v version.Version, - p platform.Platform, -) (artifact.Remote[executable.Archive], error) { - var a artifact.Remote[executable.Archive] - - if !m.Supports(v) { - return a, fmt.Errorf("%w: '%s'", ErrInvalidSpecification, v) - } - - urlVersionDir, err := urlTuxFamilyVersionDir(v) - if err != nil { - return a, err - } - - executableArchive := executable.Archive{Artifact: executable.New(v, p)} - - urlParsed, err := client.ParseURL(urlVersionDir, executableArchive.Name()) - if err != nil { - return a, errors.Join(ErrInvalidURL, err) - } - - a.Artifact, a.URL = executableArchive, urlParsed - - return a, nil -} - -func (m TuxFamily) ExecutableArchiveChecksums(v version.Version) (artifact.Remote[checksum.Executable], error) { - var a artifact.Remote[checksum.Executable] - - if !m.Supports(v) { - return a, fmt.Errorf("%w: '%s'", ErrInvalidSpecification, v.String()) - } - - checksumsExecutable, err := checksum.NewExecutable(v) - if err != nil { - return a, errors.Join(ErrInvalidSpecification, err) - } +var _ Hoster = (*TuxFamily[artifact.Versioned])(nil) +var _ Remoter[artifact.Versioned] = (*TuxFamily[artifact.Versioned])(nil) - urlVersionDir, err := urlTuxFamilyVersionDir(v) - if err != nil { - return a, err - } - - urlParsed, err := client.ParseURL(urlVersionDir, checksumsExecutable.Name()) - if err != nil { - return a, errors.Join(ErrInvalidURL, err) - } +/* ------------------------------ Impl: Hoster ------------------------------ */ - a.Artifact, a.URL = checksumsExecutable, urlParsed - - return a, nil +// Hosts returns the host URLs at which artifacts are hosted. +func (m TuxFamily[T]) Hosts() []string { + return []string{gitHubContentDomain} } -/* ------------------------------ Impl: Source ------------------------------ */ - -func (m TuxFamily) SourceArchive(v version.Version) (artifact.Remote[source.Archive], error) { - var a artifact.Remote[source.Archive] +/* ------------------------------ Impl: Remoter ----------------------------- */ - if !m.Supports(v) { - return a, fmt.Errorf("%w: '%s'", ErrInvalidSpecification, v.String()) - } +// Remote returns an 'artifact.Remote' wrapper around a specified artifact. The +// remote wrapper contains the URL at which the artifact can be downloaded. +func (m TuxFamily[T]) Remote(a T) (artifact.Remote[T], error) { + var remote artifact.Remote[T] - urlVersionDir, err := urlTuxFamilyVersionDir(v) - if err != nil { - return a, err + switch any(a).(type) { // FIXME: https://github.com/golang/go/issues/45380 + case executable.Archive, source.Archive: + case checksum.Executable, checksum.Source: + default: + return remote, fmt.Errorf("%w: %T", ErrUnsupportedArtifact, a) } - s := source.New(v) - sourceArchive := source.Archive{Artifact: s} + urlVersionDir := urlTuxFamilyVersionDir(a.Version()) - urlParsed, err := client.ParseURL(urlVersionDir, sourceArchive.Name()) + urlParsed, err := client.ParseURL(urlVersionDir, a.Name()) if err != nil { - return a, errors.Join(ErrInvalidURL, err) + return remote, errors.Join(ErrInvalidURL, err) } - a.Artifact, a.URL = sourceArchive, urlParsed + remote.Artifact, remote.URL = a, urlParsed - return a, nil + return remote, nil } -func (m TuxFamily) SourceArchiveChecksums(v version.Version) (artifact.Remote[checksum.Source], error) { - var a artifact.Remote[checksum.Source] - - if !m.Supports(v) { - return a, fmt.Errorf("%w: '%s'", ErrInvalidSpecification, v.String()) - } - - checksumsSource, err := checksum.NewSource(v) - if err != nil { - return a, errors.Join(ErrInvalidSpecification, err) - } - - urlVersionDir, err := urlTuxFamilyVersionDir(v) - if err != nil { - return a, err - } - - urlParsed, err := client.ParseURL(urlVersionDir, checksumsSource.Name()) - if err != nil { - return a, errors.Join(ErrInvalidURL, err) - } - - a.Artifact, a.URL = checksumsSource, urlParsed +/* ------------------------------ Impl: Mirror ------------------------------ */ - return a, nil +// Name returns the display name of the mirror. +func (m TuxFamily[T]) Name() string { + return "TuxFamily (downloads.tuxfamily.org/godotengine)" } /* -------------------- Function: urlTuxFamilyVersionDir -------------------- */ @@ -178,7 +88,7 @@ func (m TuxFamily) SourceArchiveChecksums(v version.Version) (artifact.Remote[ch // route is built up in parts by replicating the directory structure. It's // possible some edge cases are mishandled; please open an issue if one's found: // https://github.com/coffeebeats/gdenv/issues/new?assignees=&labels=bug&projects=&template=%F0%9F%90%9B-bug-report.md -func urlTuxFamilyVersionDir(v version.Version) (string, error) { +func urlTuxFamilyVersionDir(v version.Version) string { p := make([]string, 0) // The first directory will be the "normal version", but a patch version of @@ -208,10 +118,10 @@ func urlTuxFamilyVersionDir(v version.Version) (string, error) { p = append(p, v.Label()) } - urlVersionDir, err := url.JoinPath(tuxFamilyAssetsURLBase, p...) + versionDirURL, err := url.JoinPath(tuxFamilyAssetsURLBase, p...) if err != nil { - return "", errors.Join(ErrInvalidURL, err) + panic(err) // This indicates an error in the asset URL base constant. } - return urlVersionDir, nil + return versionDirURL } diff --git a/pkg/godot/mirror/tuxfamily_test.go b/pkg/godot/mirror/tuxfamily_test.go index 12c872da..b19b1cc4 100644 --- a/pkg/godot/mirror/tuxfamily_test.go +++ b/pkg/godot/mirror/tuxfamily_test.go @@ -7,188 +7,80 @@ import ( "testing" "github.com/coffeebeats/gdenv/pkg/godot/artifact" - "github.com/coffeebeats/gdenv/pkg/godot/artifact/checksum" + "github.com/coffeebeats/gdenv/pkg/godot/artifact/artifacttest" "github.com/coffeebeats/gdenv/pkg/godot/artifact/executable" "github.com/coffeebeats/gdenv/pkg/godot/artifact/source" "github.com/coffeebeats/gdenv/pkg/godot/version" ) -/* ----------------------- TuxFamily.ExecutableArchive ---------------------- */ +/* ------------------------- Test: TuxFamily.Remote ------------------------- */ -func TestTuxFamilyExecutableArchive(t *testing.T) { +func TestTuxFamilyRemote(t *testing.T) { tests := []struct { - ex executable.Executable - name string - url *url.URL - err error + artifact artifact.Versioned + name string + + url *url.URL + err error }{ // Invalid inputs - {ex: executable.Executable{}, err: ErrInvalidSpecification}, - {ex: executable.MustParse("Godot_v0.0.0-stable_linux.x86_64"), err: ErrInvalidSpecification}, + {artifact: artifacttest.MockArtifact{}, err: ErrUnsupportedArtifact}, // Valid inputs { - ex: executable.MustParse("Godot_v4.1.1-stable_mono_linux_x86_64"), - name: "Godot_v4.1.1-stable_mono_linux_x86_64.zip", - url: mustParseURL(t, "https://downloads.tuxfamily.org/godotengine/4.1.1/mono/Godot_v4.1.1-stable_mono_linux_x86_64.zip"), + artifact: executable.Archive{Artifact: executable.MustParse("Godot_v4.1.1-stable_mono_linux.x86_64")}, + url: mustParseURL(t, tuxFamilyAssetsURLBase+"/4.1.1/mono/Godot_v4.1.1-stable_mono_linux_x86_64.zip"), }, { - ex: executable.MustParse("Godot_v4.1-stable_linux.x86_64"), - name: "Godot_v4.1-stable_linux.x86_64.zip", - url: mustParseURL(t, "https://downloads.tuxfamily.org/godotengine/4.1/Godot_v4.1-stable_linux.x86_64.zip"), + artifact: executable.Archive{Artifact: executable.MustParse("Godot_v4.1-stable_linux.x86_64")}, + url: mustParseURL(t, tuxFamilyAssetsURLBase+"/4.1/Godot_v4.1-stable_linux.x86_64.zip"), }, { - ex: executable.MustParse("Godot_v4.0-dev.20220118_win64.exe"), - name: "Godot_v4.0-dev.20220118_win64.exe.zip", - url: mustParseURL(t, "https://downloads.tuxfamily.org/godotengine/4.0/pre-alpha/4.0-dev.20220118/Godot_v4.0-dev.20220118_win64.exe.zip"), + artifact: executable.Archive{Artifact: executable.MustParse("Godot_v4.0-dev.20220118_win64.exe")}, + url: mustParseURL(t, tuxFamilyAssetsURLBase+"/4.0/pre-alpha/4.0-dev.20220118/Godot_v4.0-dev.20220118_win64.exe.zip"), }, - } - - for _, tc := range tests { - t.Run(tc.ex.String(), func(t *testing.T) { - got, err := (&TuxFamily{}).ExecutableArchive(tc.ex.Version(), tc.ex.Platform()) - - if !errors.Is(err, tc.err) { - t.Errorf("err: got %v, want %v", err, tc.err) - } - - if got := got.Artifact.Name(); got != tc.name { - t.Errorf("output: got %v, want %v", got, tc.name) - } - if got := got.URL; !reflect.DeepEqual(got, tc.url) { - t.Errorf("output: got %v, want %v", got, tc.url) - } - }) - } -} - -/* --------------- Test: TuxFamily.ExecutableArchiveChecksums --------------- */ - -func TestTuxFamilyExecutableArchiveChecksums(t *testing.T) { - tests := []struct { - v version.Version - url *url.URL - err error - }{ - // Invalid inputs - {v: version.Version{}, err: ErrInvalidSpecification}, - {v: version.MustParse("v0.0.0"), err: ErrInvalidSpecification}, - - // Valid inputs { - v: version.MustParse("4.1.1-stable"), - url: mustParseURL(t, "https://downloads.tuxfamily.org/godotengine/4.1.1/SHA512-SUMS.txt"), + artifact: source.Archive{Artifact: source.New(version.MustParse("4.1.1-stable"))}, + url: mustParseURL(t, tuxFamilyAssetsURLBase+"/4.1.1/godot-4.1.1-stable.tar.xz"), }, { - v: version.MustParse("4.1-stable"), - url: mustParseURL(t, "https://downloads.tuxfamily.org/godotengine/4.1/SHA512-SUMS.txt"), + artifact: source.Archive{Artifact: source.New(version.MustParse("4.1.0-stable"))}, + url: mustParseURL(t, tuxFamilyAssetsURLBase+"/4.1/godot-4.1-stable.tar.xz"), }, { - v: version.MustParse("4.0-dev.20220118"), - url: mustParseURL(t, "https://downloads.tuxfamily.org/godotengine/4.0/pre-alpha/4.0-dev.20220118/SHA512-SUMS.txt"), + artifact: mustMakeNewExecutableChecksum(t, version.MustParse("4.1.1-stable")), + url: mustParseURL(t, tuxFamilyAssetsURLBase+"/4.1.1/SHA512-SUMS.txt"), }, - } - - for _, tc := range tests { - t.Run(tc.v.String(), func(t *testing.T) { - got, err := (&TuxFamily{}).ExecutableArchiveChecksums(tc.v) - - if !errors.Is(err, tc.err) { - t.Errorf("err: got %#v, want %#v", err, tc.err) - } - - // The test setup below will fail for invalid inputs. - if tc.url == nil { - return - } - - ex, err := checksum.NewExecutable(tc.v) - if err != nil { - t.Fatalf("test setup: %v", err) - } - - want := artifact.Remote[checksum.Executable]{Artifact: ex, URL: tc.url} - if !reflect.DeepEqual(got, want) { - t.Errorf("output: got %#v, want %#v", got, want) - } - }) - } -} - -/* ---------------------- Test: TuxFamily.SourceArchive --------------------- */ - -func TestTuxFamilySourceArchive(t *testing.T) { - tests := []struct { - v version.Version - - artifact source.Archive - url *url.URL - err error - }{ - // Invalid inputs - {v: version.Version{}, err: ErrInvalidSpecification}, - {v: version.MustParse("v0.1.0"), err: ErrInvalidSpecification}, - - // Valid inputs { - v: version.MustParse("v4.1.1"), - artifact: source.Archive{Artifact: source.New(version.MustParse("v4.1.1"))}, - url: mustParseURL(t, "https://downloads.tuxfamily.org/godotengine/4.1.1/godot-4.1.1-stable.tar.xz"), + artifact: mustMakeNewExecutableChecksum(t, version.MustParse("4.1.0-stable")), + url: mustParseURL(t, tuxFamilyAssetsURLBase+"/4.1/SHA512-SUMS.txt"), }, - } - - for _, tc := range tests { - t.Run(tc.v.String(), func(t *testing.T) { - remote, err := (&TuxFamily{}).SourceArchive(tc.v) - - if !errors.Is(err, tc.err) { - t.Errorf("err: got %v, want %v", err, tc.err) - } - - if got := remote.Artifact; !reflect.DeepEqual(got, tc.artifact) { - t.Errorf("output: got %v, want %v", got, tc.artifact) - } - if got := remote.URL; !reflect.DeepEqual(got, tc.url) { - t.Errorf("output: got %v, want %v", got, tc.url) - } - }) - } -} - -/* ----------------- Test: TuxFamily.SourceArchiveChecksums ----------------- */ - -func TestTuxFamilySourceArchiveChecksums(t *testing.T) { - tests := []struct { - v version.Version - - artifact checksum.Source - url *url.URL - err error - }{ - // Invalid inputs - {v: version.Version{}, err: ErrInvalidSpecification}, - {v: version.MustParse("v0.1.0"), err: ErrInvalidSpecification}, - - // Valid inputs { - v: version.MustParse("v4.1.1"), - artifact: mustMakeNewSource(t, version.MustParse("v4.1.1")), - url: mustParseURL(t, "https://downloads.tuxfamily.org/godotengine/4.1.1/godot-4.1.1-stable.tar.xz.sha256"), + artifact: mustMakeNewExecutableChecksum(t, version.MustParse("4.0-dev.20220118")), + url: mustParseURL(t, tuxFamilyAssetsURLBase+"/4.0/pre-alpha/4.0-dev.20220118/SHA512-SUMS.txt"), + }, + { + artifact: mustMakeNewSourceChecksum(t, version.MustParse("4.1.1-stable")), + url: mustParseURL(t, tuxFamilyAssetsURLBase+"/4.1.1/godot-4.1.1-stable.tar.xz.sha256"), + }, + { + artifact: mustMakeNewSourceChecksum(t, version.MustParse("4.1.0-stable")), + url: mustParseURL(t, tuxFamilyAssetsURLBase+"/4.1/godot-4.1-stable.tar.xz.sha256"), }, } for _, tc := range tests { - t.Run(tc.v.String(), func(t *testing.T) { - remote, err := (&TuxFamily{}).SourceArchiveChecksums(tc.v) + t.Run(tc.artifact.Name(), func(t *testing.T) { + got, err := (&TuxFamily[artifact.Versioned]{}).Remote(tc.artifact) if !errors.Is(err, tc.err) { t.Errorf("err: got %v, want %v", err, tc.err) } - if got := remote.Artifact; !reflect.DeepEqual(got, tc.artifact) { - t.Errorf("output: got %v, want %v", got, tc.artifact) + if got := got.Artifact; got != nil && got.Name() != tc.artifact.Name() { + t.Errorf("output: got %v, want %v", got, tc.artifact.Name()) } - if got := remote.URL; !reflect.DeepEqual(got, tc.url) { + if got := got.URL; !reflect.DeepEqual(got, tc.url) { t.Errorf("output: got %v, want %v", got, tc.url) } }) diff --git a/pkg/install/install.go b/pkg/install/install.go index b2321c55..66be881f 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -2,10 +2,8 @@ package install import ( "context" - "fmt" "os" "path/filepath" - "strings" "github.com/charmbracelet/log" "github.com/coffeebeats/gdenv/pkg/download" @@ -13,8 +11,6 @@ import ( "github.com/coffeebeats/gdenv/pkg/godot/artifact/archive" "github.com/coffeebeats/gdenv/pkg/godot/artifact/executable" "github.com/coffeebeats/gdenv/pkg/godot/artifact/source" - "github.com/coffeebeats/gdenv/pkg/godot/mirror" - "github.com/coffeebeats/gdenv/pkg/godot/platform" "github.com/coffeebeats/gdenv/pkg/store" ) @@ -24,13 +20,6 @@ import ( // Downloads and caches a platform-specific version of Godot. func Executable(ctx context.Context, storePath string, ex executable.Executable) error { - m, err := mirror.Select(ctx, ex.Version(), ex.Platform(), availableMirrors()) - if err != nil { - return err - } - - log.Infof("downloading from mirror: %s", strings.TrimPrefix(fmt.Sprintf("%T", m), "mirror.")) - tmp, err := os.MkdirTemp("", "gdenv-*") if err != nil { return err @@ -40,7 +29,7 @@ func Executable(ctx context.Context, storePath string, ex executable.Executable) log.Debugf("using temporary directory: %s", tmp) - localExArchive, err := download.ExecutableWithChecksumValidation(ctx, m, ex, tmp) + localExArchive, err := download.ExecutableWithChecksumValidation(ctx, ex, tmp) if err != nil { return err } @@ -79,18 +68,6 @@ func Executable(ctx context.Context, storePath string, ex executable.Executable) // Downloads and caches a specific version of Godot's source code. func Source(ctx context.Context, storePath string, src source.Source) error { - // TODO: Make this not rely on this (arbitrary) platform. It would be better - // if 'mirror.checkIfExists' could correctly determine existence of an - // arbitrary artifact. For now, select a platform that will certainly exist. - p := platform.Platform{Arch: platform.Amd64, OS: platform.Windows} - - m, err := mirror.Select(ctx, src.Version(), p, availableMirrors()) - if err != nil { - return err - } - - log.Infof("downloading from mirror: %s", strings.TrimPrefix(fmt.Sprintf("%T", m), "mirror.")) - tmp, err := os.MkdirTemp("", "gdenv-*") if err != nil { return err @@ -100,7 +77,7 @@ func Source(ctx context.Context, storePath string, src source.Source) error { log.Debugf("using temporary directory: %s", tmp) - localSourceArchive, err := download.SourceWithChecksumValidation(ctx, m, src.Version(), tmp) + localSourceArchive, err := download.SourceWithChecksumValidation(ctx, src.Version(), tmp) if err != nil { return err } @@ -115,11 +92,3 @@ func Source(ctx context.Context, storePath string, src source.Source) error { }, ) } - -/* ----------------------- Function: availableMirrors ----------------------- */ - -// availableMirrors returns the list of possible 'Mirror' hosts to use for -// downloads. -func availableMirrors() []mirror.Mirror { - return []mirror.Mirror{mirror.GitHub{}, mirror.TuxFamily{}} -} diff --git a/pkg/progress/writer.go b/pkg/progress/writer.go index d76455f1..18ad4b72 100644 --- a/pkg/progress/writer.go +++ b/pkg/progress/writer.go @@ -13,7 +13,7 @@ type Writer struct { } // Validate at compile-time that 'Writer' implements 'io.Writer'. -var _ io.Writer = &Writer{} //nolint:exhaustruct +var _ io.Writer = (*Writer)(nil) /* --------------------------- Function: NewWriter -------------------------- */