diff --git a/internal/buildpack/buildpack.go b/internal/buildpack/buildpack.go index dc23daff..a1cd78b7 100644 --- a/internal/buildpack/buildpack.go +++ b/internal/buildpack/buildpack.go @@ -52,6 +52,7 @@ var packs = map[string]func(context.Context, Sys, yb.BuildpackSpec) (biome.Envir "r": installR, "ruby": installRuby, "rust": installRust, + "sbt": installSBT, "yarn": installYarn, } @@ -101,14 +102,18 @@ func extract(ctx context.Context, sys Sys, dstDir, url string, extractMode bool) zipExt = ".zip" tarXZExt = ".tar.xz" tarGZExt = ".tar.gz" + tgzExt = ".tgz" tarBZ2Ext = ".tar.bz2" + tbz2Ext = ".tbz2" ) const cleanupTimeout = 10 * time.Second exts := []string{ zipExt, tarXZExt, tarGZExt, + tgzExt, tarBZ2Ext, + tbz2Ext, } var ext string for _, testExt := range exts { @@ -183,7 +188,7 @@ func extract(ctx context.Context, sys Sys, dstDir, url string, extractMode bool) if extractMode == stripTopDirectory { invoke.Argv = append(invoke.Argv, "--strip-components", "1") } - case tarGZExt: + case tarGZExt, tgzExt: invoke.Argv = []string{ "tar", "-x", // extract @@ -193,7 +198,7 @@ func extract(ctx context.Context, sys Sys, dstDir, url string, extractMode bool) if extractMode == stripTopDirectory { invoke.Argv = append(invoke.Argv, "--strip-components", "1") } - case tarBZ2Ext: + case tarBZ2Ext, tbz2Ext: invoke.Argv = []string{ "tar", "-x", // extract diff --git a/internal/buildpack/sbt.go b/internal/buildpack/sbt.go new file mode 100644 index 00000000..89c47bce --- /dev/null +++ b/internal/buildpack/sbt.go @@ -0,0 +1,111 @@ +// Copyright 2021 YourBase Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package buildpack + +import ( + "context" + "fmt" + "strings" + "os" + "io/ioutil" + + "github.com/yourbase/yb" + "github.com/yourbase/yb/internal/biome" + "zombiezen.com/go/log" +) + +func installSBT(ctx context.Context, sys Sys, spec yb.BuildpackSpec) (_ biome.Environment, err error) { + installDir := sys.Biome.JoinPath(sys.Biome.Dirs().Tools, "sbt", spec.Version()) + desc := sys.Biome.Describe() + parentDir := os.TempDir() + socketDir, err := ioutil.TempDir(parentDir, "*-ybsbt") + if err != nil { + log.Errorf(ctx, "Error creating directory for SBT sockets: %v", err) + } + log.Infof(ctx, "SBT will use %s as the socket directory", socketDir) +// defer os.RemoveAll(socketDir) // clean up + + env := biome.Environment{ + Vars: map[string]string{ + "SBT_GLOBAL_SERVER_DIR": socketDir, + }, + PrependPath: []string{sys.Biome.JoinPath(installDir)}, + } + + // If directory already exists, then use it. + if _, err := biome.EvalSymlinks(ctx, sys.Biome, installDir); err == nil { + log.Infof(ctx, "SBT v%s located in %s", spec.Version(), installDir) + return env, nil + } + + log.Infof(ctx, "Installing SBT v%s in %s", spec.Version(), installDir) + downloadURL, err := sbtDownloadURL(spec.Version(), desc) + if err != nil { + return biome.Environment{}, err + } + if err := extract(ctx, sys, installDir, downloadURL, stripTopDirectory); err != nil { + return biome.Environment{}, err + } + return env, nil +} + +func sbtDownloadURL(version string, desc *biome.Descriptor) (string, error) { + vparts := strings.SplitN(version, "+", 2) + subVersion := "" + if len(vparts) > 1 { + subVersion = vparts[1] + version = vparts[0] + } + + parts := strings.Split(version, ".") + + majorVersion, err := convertVersionPiece(parts, 0) + if err != nil { + return "", fmt.Errorf("parse jdk version %q: major: %w", version, err) + } + minorVersion, err := convertVersionPiece(parts, 1) + if err != nil { + return "", fmt.Errorf("parse jdk version %q: minor: %w", version, err) + } + patchVersion, err := convertVersionPiece(parts, 2) + if err != nil { + return "", fmt.Errorf("parse jdk version %q: patch: %w", version, err) + } + + urlPattern := "https://github.com/sbt/sbt/releases/download/v{{ .MajorVersion }}.{{ .MinorVersion }}.{{ .PatchVersion }}/sbt-{{ .MajorVersion }}.{{ .MinorVersion }}.{{ .PatchVersion }}.tgz" + + var data struct { + OS string + Arch string + MajorVersion int64 + MinorVersion int64 + PatchVersion int64 + SubVersion string // not always an int, sometimes a float + } + data.OS = map[string]string{ + biome.Linux: "linux", + biome.MacOS: "mac", + }[desc.OS] + if data.OS == "" { + return "", fmt.Errorf("unsupported os %s", desc.OS) + } + data.MajorVersion = majorVersion + data.MinorVersion = minorVersion + data.PatchVersion = patchVersion + data.SubVersion = subVersion + return templateToString(urlPattern, data) +} diff --git a/internal/buildpack/sbt_test.go b/internal/buildpack/sbt_test.go new file mode 100644 index 00000000..a737dc90 --- /dev/null +++ b/internal/buildpack/sbt_test.go @@ -0,0 +1,46 @@ +// Copyright 2021 YourBase Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package buildpack + +import ( + "context" + "strings" + "testing" + + "github.com/yourbase/yb/internal/biome" + "zombiezen.com/go/log/testlog" +) + +func TestSBT(t *testing.T) { + const version = "1.5.2" + ctx := testlog.WithTB(context.Background(), t) + // TODO(johnewart): SBT depends on Java -- see Ross's comment in sbt tests + sbtBiome, _ := testInstall(ctx, t, "java:8.252.10", "sbt:"+version) + versionOutput := new(strings.Builder) + err := sbtBiome.Run(ctx, &biome.Invocation{ + Argv: []string{"sbt", "--version"}, + Stdout: versionOutput, + Stderr: versionOutput, + }) + t.Logf("sbt --version output:\n%s", versionOutput) + if err != nil { + t.Errorf("sbt --version: %v", err) + } + if got := versionOutput.String(); !strings.Contains(got, version) { + t.Errorf("sbt --version output does not include %q", version) + } +}