From 9eb50e2242525b877c54c0546624bcbd1a68f99c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:06:15 +0000 Subject: [PATCH 1/3] Initial plan From 9d060123840854908e1bb6c829fca15e21204d4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:21:40 +0000 Subject: [PATCH 2/3] Add Java feature with Maven, Gradle, and Ant support Agent-Logs-Url: https://github.com/postfinance/devcontainer-features/sessions/5629081a-8a0a-49d2-8806-eb80887989b9 Co-authored-by: Roemer <393641+Roemer@users.noreply.github.com> --- README.md | 1 + build/build.go | 4 + features/src/java/NOTES.md | 19 + features/src/java/README.md | 69 ++++ features/src/java/devcontainer-feature.json | 124 ++++++ features/src/java/install.sh | 16 + features/src/java/installer.go | 397 ++++++++++++++++++++ features/test/java/install-exact.sh | 9 + features/test/java/install-latest.sh | 9 + features/test/java/install-with-ant.sh | 12 + features/test/java/install-with-gradle.sh | 12 + features/test/java/install-with-maven.sh | 12 + features/test/java/install-with-resolve.sh | 9 + features/test/java/scenarios.json | 83 ++++ features/test/java/test-images.json | 4 + override-all.env | 11 + 16 files changed, 791 insertions(+) create mode 100644 features/src/java/NOTES.md create mode 100755 features/src/java/README.md create mode 100644 features/src/java/devcontainer-feature.json create mode 100644 features/src/java/install.sh create mode 100644 features/src/java/installer.go create mode 100644 features/test/java/install-exact.sh create mode 100644 features/test/java/install-latest.sh create mode 100644 features/test/java/install-with-ant.sh create mode 100644 features/test/java/install-with-gradle.sh create mode 100644 features/test/java/install-with-maven.sh create mode 100644 features/test/java/install-with-resolve.sh create mode 100644 features/test/java/scenarios.json create mode 100644 features/test/java/test-images.json diff --git a/README.md b/README.md index a28aa5b..9265855 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Below is a list with included features, click on the link for more details. | [gonovate](./features/src/gonovate/README.md) | Installs Gonovate. | | [goreleaser](./features/src/goreleaser/README.md) | Installs GoReleaser. | | [instant-client](./features/src/instant-client/README.md) | Installs the Oracle Instant Client Basic package. | +| [java](./features/src/java/README.md) | Installs Java (Temurin/OpenJDK), Maven, Gradle, and Ant. | | [jfrog-cli](./features/src/jfrog-cli/README.md) | Installs the JFrog CLI. | | [kubectl](./features/src/kubectl/README.md) | Installs kubectl and other tools for managing kubernetes. | | [locale](./features/src/locale/README.md) | Allows setting the locale. | diff --git a/build/build.go b/build/build.go index 4f5e0e7..97437bb 100644 --- a/build/build.go +++ b/build/build.go @@ -134,6 +134,10 @@ func init() { gotaskr.Task("Feature:gonovate:Package", func() error { return packageFeature("gonovate") }) gotaskr.Task("Feature:gonovate:Test", func() error { return testFeature("gonovate") }) + ////////// java + gotaskr.Task("Feature:java:Package", func() error { return packageFeature("java") }) + gotaskr.Task("Feature:java:Test", func() error { return testFeature("java") }) + ////////// goreleaser gotaskr.Task("Feature:goreleaser:Package", func() error { return packageFeature("goreleaser") }) gotaskr.Task("Feature:goreleaser:Test", func() error { return testFeature("goreleaser") }) diff --git a/features/src/java/NOTES.md b/features/src/java/NOTES.md new file mode 100644 index 0000000..5894651 --- /dev/null +++ b/features/src/java/NOTES.md @@ -0,0 +1,19 @@ +## Notes + +### System Compatibility + +Debian, Ubuntu + +### Accessed Urls + +Needs access to the following URLs for downloading and resolving Java (Temurin/OpenJDK): +* https://api.adoptium.net + +Needs access to the following URL for downloading and resolving Maven: +* https://downloads.apache.org/maven + +Needs access to the following URLs for downloading and resolving Gradle: +* https://services.gradle.org + +Needs access to the following URL for downloading and resolving Ant: +* https://downloads.apache.org/ant diff --git a/features/src/java/README.md b/features/src/java/README.md new file mode 100755 index 0000000..f501a0b --- /dev/null +++ b/features/src/java/README.md @@ -0,0 +1,69 @@ +# Java (java) + +Installs Java (Temurin/OpenJDK), Maven, Gradle, and Ant. + +## Example Usage + +```json +"features": { + "ghcr.io/postfinance/devcontainer-features/java:1.0.0": { + "version": "latest", + "mavenVersion": "none", + "gradleVersion": "none", + "antVersion": "none", + "downloadUrl": "", + "versionsUrl": "", + "latestUrl": "", + "mavenDownloadUrl": "", + "mavenVersionsUrl": "", + "gradleDownloadUrl": "", + "gradleVersionsUrl": "", + "antDownloadUrl": "", + "antVersionsUrl": "" + } +} +``` + +## Options + +| Option | Description | Type | Default Value | Proposals | +|-----|-----|-----|-----|-----| +| version | The version of Java (Temurin/OpenJDK) to install. Use a major version (e.g. '21') to get the latest patch release. | string | latest | latest, 21, 17, 11, 21.0.5 | +| mavenVersion | The version of Maven to install. Use 'none' to skip. | string | none | none, latest, 3.9, 3.9.9 | +| gradleVersion | The version of Gradle to install. Use 'none' to skip. | string | none | none, latest, 8, 8.14 | +| antVersion | The version of Ant to install. Use 'none' to skip. | string | none | none, latest, 1.10, 1.10.15 | +| downloadUrl | The download URL to use for Java (Temurin) binaries. | string | <empty> | https://mycompany.com/artifactory/adoptium-remote/v3/binary | +| versionsUrl | The URL to fetch the available Java (Temurin) versions from. | string | <empty> | | +| latestUrl | The URL to fetch the latest Java (Temurin) release information from. | string | <empty> | | +| mavenDownloadUrl | The download URL to use for Maven binaries. | string | <empty> | https://mycompany.com/artifactory/apache-maven-remote | +| mavenVersionsUrl | The URL to fetch the available Maven versions from. | string | <empty> | | +| gradleDownloadUrl | The download URL to use for Gradle binaries. | string | <empty> | https://mycompany.com/artifactory/gradle-distributions-remote | +| gradleVersionsUrl | The URL to fetch the available Gradle versions from. | string | <empty> | | +| antDownloadUrl | The download URL to use for Ant binaries. | string | <empty> | https://mycompany.com/artifactory/apache-ant-remote | +| antVersionsUrl | The URL to fetch the available Ant versions from. | string | <empty> | | + +## Customizations + +### VS Code Extensions + +- `vscjava.vscode-java-pack` + +## Notes + +### System Compatibility + +Debian, Ubuntu + +### Accessed Urls + +Needs access to the following URLs for downloading and resolving Java (Temurin/OpenJDK): +* https://api.adoptium.net + +Needs access to the following URL for downloading and resolving Maven: +* https://downloads.apache.org/maven + +Needs access to the following URLs for downloading and resolving Gradle: +* https://services.gradle.org + +Needs access to the following URL for downloading and resolving Ant: +* https://downloads.apache.org/ant diff --git a/features/src/java/devcontainer-feature.json b/features/src/java/devcontainer-feature.json new file mode 100644 index 0000000..0b539c1 --- /dev/null +++ b/features/src/java/devcontainer-feature.json @@ -0,0 +1,124 @@ +{ + "id": "java", + "version": "1.0.0", + "name": "Java", + "description": "Installs Java (Temurin/OpenJDK), Maven, Gradle, and Ant.", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest", + "21", + "17", + "11", + "21.0.5" + ], + "default": "latest", + "description": "The version of Java (Temurin/OpenJDK) to install. Use a major version (e.g. '21') to get the latest patch release." + }, + "mavenVersion": { + "type": "string", + "proposals": [ + "none", + "latest", + "3.9", + "3.9.9" + ], + "default": "none", + "description": "The version of Maven to install. Use 'none' to skip." + }, + "gradleVersion": { + "type": "string", + "proposals": [ + "none", + "latest", + "8", + "8.14" + ], + "default": "none", + "description": "The version of Gradle to install. Use 'none' to skip." + }, + "antVersion": { + "type": "string", + "proposals": [ + "none", + "latest", + "1.10", + "1.10.15" + ], + "default": "none", + "description": "The version of Ant to install. Use 'none' to skip." + }, + "downloadUrl": { + "type": "string", + "default": "", + "proposals": [ + "https://mycompany.com/artifactory/adoptium-remote/v3/binary" + ], + "description": "The download URL to use for Java (Temurin) binaries." + }, + "versionsUrl": { + "type": "string", + "default": "", + "description": "The URL to fetch the available Java (Temurin) versions from." + }, + "latestUrl": { + "type": "string", + "default": "", + "description": "The URL to fetch the latest Java (Temurin) release information from." + }, + "mavenDownloadUrl": { + "type": "string", + "default": "", + "proposals": [ + "https://mycompany.com/artifactory/apache-maven-remote" + ], + "description": "The download URL to use for Maven binaries." + }, + "mavenVersionsUrl": { + "type": "string", + "default": "", + "description": "The URL to fetch the available Maven versions from." + }, + "gradleDownloadUrl": { + "type": "string", + "default": "", + "proposals": [ + "https://mycompany.com/artifactory/gradle-distributions-remote" + ], + "description": "The download URL to use for Gradle binaries." + }, + "gradleVersionsUrl": { + "type": "string", + "default": "", + "description": "The URL to fetch the available Gradle versions from." + }, + "antDownloadUrl": { + "type": "string", + "default": "", + "proposals": [ + "https://mycompany.com/artifactory/apache-ant-remote" + ], + "description": "The download URL to use for Ant binaries." + }, + "antVersionsUrl": { + "type": "string", + "default": "", + "description": "The URL to fetch the available Ant versions from." + } + }, + "customizations": { + "vscode": { + "extensions": [ + "vscjava.vscode-java-pack" + ] + } + }, + "containerEnv": { + "JAVA_HOME": "/usr/local/java", + "MAVEN_HOME": "/usr/local/maven", + "GRADLE_HOME": "/usr/local/gradle", + "ANT_HOME": "/usr/local/ant", + "PATH": "/usr/local/java/bin:/usr/local/maven/bin:/usr/local/gradle/bin:/usr/local/ant/bin:${PATH}" + } +} diff --git a/features/src/java/install.sh b/features/src/java/install.sh new file mode 100644 index 0000000..c7137d4 --- /dev/null +++ b/features/src/java/install.sh @@ -0,0 +1,16 @@ +. ./functions.sh + +"./installer_$(detect_arch)" \ + -version="${VERSION:-"latest"}" \ + -mavenVersion="${MAVENVERSION:-"none"}" \ + -gradleVersion="${GRADLEVERSION:-"none"}" \ + -antVersion="${ANTVERSION:-"none"}" \ + -downloadUrl="${DOWNLOADURL:-""}" \ + -versionsUrl="${VERSIONSURL:-""}" \ + -latestUrl="${LATESTURL:-""}" \ + -mavenDownloadUrl="${MAVENDOWNLOADURL:-""}" \ + -mavenVersionsUrl="${MAVENVERSIONSURL:-""}" \ + -gradleDownloadUrl="${GRADLEDOWNLOADURL:-""}" \ + -gradleVersionsUrl="${GRADLEVERSIONSURL:-""}" \ + -antDownloadUrl="${ANTDOWNLOADURL:-""}" \ + -antVersionsUrl="${ANTVERSIONSURL:-""}" diff --git a/features/src/java/installer.go b/features/src/java/installer.go new file mode 100644 index 0000000..6a2e8bb --- /dev/null +++ b/features/src/java/installer.go @@ -0,0 +1,397 @@ +package main + +import ( + "builder/installer" + "encoding/json" + "flag" + "fmt" + "os" + "regexp" + + "github.com/roemer/gover" +) + +////////// +// Configuration +////////// + +// Version regex for Temurin/OpenJDK releases like "jdk-21.0.7+6" +var javaVersionRegex *regexp.Regexp = regexp.MustCompile(`^jdk-(?P(\d+)\.(\d+)\.(\d+))(?:\+\d+)?$`) + +// Version regex for Maven releases like "3.9.9" +var mavenVersionRegex *regexp.Regexp = regexp.MustCompile(`^(?P(\d+)\.(\d+)\.(\d+))$`) + +// Version regex for Gradle releases like "8.14" or "8.2.1" +var gradleVersionRegex *regexp.Regexp = regexp.MustCompile(`^(?P(\d+)\.(\d+)(?:\.(\d+))?)$`) + +// Version regex for Ant releases like "1.10.15" +var antVersionRegex *regexp.Regexp = regexp.MustCompile(`^(?P(\d+)\.(\d+)\.(\d+))$`) + +// Line regex for parsing Maven HTML index pages like href="3.9.9/" +var mavenIndexLineRegex *regexp.Regexp = regexp.MustCompile(`href="(\d+\.\d+\.\d+)/"`) + +// Line regex for parsing Ant HTML index pages like href="apache-ant-1.10.15-bin.tar.gz" +var antIndexLineRegex *regexp.Regexp = regexp.MustCompile(`href="apache-ant-(\d+\.\d+\.\d+)-bin\.tar\.gz"`) + +////////// +// Main +////////// + +func main() { + if err := runMain(); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +} + +func runMain() error { + // Handle the flags + version := flag.String("version", "latest", "") + mavenVersion := flag.String("mavenVersion", "none", "") + gradleVersion := flag.String("gradleVersion", "none", "") + antVersion := flag.String("antVersion", "none", "") + downloadUrl := flag.String("downloadUrl", "", "") + versionsUrl := flag.String("versionsUrl", "", "") + latestUrl := flag.String("latestUrl", "", "") + mavenDownloadUrl := flag.String("mavenDownloadUrl", "", "") + mavenVersionsUrl := flag.String("mavenVersionsUrl", "", "") + gradleDownloadUrl := flag.String("gradleDownloadUrl", "", "") + gradleVersionsUrl := flag.String("gradleVersionsUrl", "", "") + antDownloadUrl := flag.String("antDownloadUrl", "", "") + antVersionsUrl := flag.String("antVersionsUrl", "", "") + flag.Parse() + + // Load settings from an external file + if err := installer.LoadOverrides(); err != nil { + return err + } + + installer.HandleOverride(downloadUrl, "https://api.adoptium.net/v3/binary", "java-download-url") + installer.HandleOverride(versionsUrl, "https://api.adoptium.net/v3/info/release_names", "java-versions-url") + installer.HandleOverride(latestUrl, "https://api.adoptium.net/v3/info/available_releases", "java-latest-url") + installer.HandleOverride(mavenDownloadUrl, "https://downloads.apache.org/maven/maven-3", "java-maven-download-url") + installer.HandleOverride(mavenVersionsUrl, "https://downloads.apache.org/maven/maven-3/", "java-maven-versions-url") + installer.HandleOverride(gradleDownloadUrl, "https://services.gradle.org/distributions", "java-gradle-download-url") + installer.HandleOverride(gradleVersionsUrl, "https://services.gradle.org/versions/all", "java-gradle-versions-url") + installer.HandleOverride(antDownloadUrl, "https://downloads.apache.org/ant/binaries", "java-ant-download-url") + installer.HandleOverride(antVersionsUrl, "https://downloads.apache.org/ant/binaries/", "java-ant-versions-url") + + // Create and process the feature + feature := installer.NewFeature("Java", true, + &javaComponent{ + ComponentBase: installer.NewComponentBase("Java", *version), + DownloadUrl: *downloadUrl, + VersionsUrl: *versionsUrl, + LatestUrl: *latestUrl, + releaseNames: make(map[string]string), + }, + &mavenComponent{ + ComponentBase: installer.NewComponentBase("Maven", *mavenVersion), + DownloadUrl: *mavenDownloadUrl, + VersionsUrl: *mavenVersionsUrl, + }, + &gradleComponent{ + ComponentBase: installer.NewComponentBase("Gradle", *gradleVersion), + DownloadUrl: *gradleDownloadUrl, + VersionsUrl: *gradleVersionsUrl, + }, + &antComponent{ + ComponentBase: installer.NewComponentBase("Ant", *antVersion), + DownloadUrl: *antDownloadUrl, + VersionsUrl: *antVersionsUrl, + }, + ) + return feature.Process() +} + +////////// +// Java Component +////////// + +type javaComponent struct { + *installer.ComponentBase + DownloadUrl string + VersionsUrl string + LatestUrl string + releaseNames map[string]string +} + +// Always resolve through GetAllVersions to populate the release name map. +func (c *javaComponent) IsFullVersion(referenceVersion *gover.Version) bool { + return false +} + +func (c *javaComponent) GetLatestVersion() (*gover.Version, error) { + // Get available releases info to determine the most recent LTS major version + data, err := installer.Tools.Download.AsBytes(c.LatestUrl) + if err != nil { + return nil, err + } + var releasesInfo struct { + MostRecentLts int `json:"most_recent_lts"` + } + if err := json.Unmarshal(data, &releasesInfo); err != nil { + return nil, err + } + if releasesInfo.MostRecentLts == 0 { + return nil, nil + } + + // Get the latest release for the most recent LTS major version + archPart, err := installer.Tools.System.MapArchitecture(map[string]string{ + installer.AMD64: "x64", + installer.ARM64: "aarch64", + }) + if err != nil { + return nil, err + } + pageUrl := fmt.Sprintf("%s?release_type=ga&os=linux&architecture=%s&image_type=jdk&vendor=eclipse&feature_version=%d&page=0&page_size=1", + c.VersionsUrl, archPart, releasesInfo.MostRecentLts) + data, err = installer.Tools.Download.AsBytes(pageUrl) + if err != nil { + return nil, err + } + var response struct { + Releases []string `json:"releases"` + } + if err := json.Unmarshal(data, &response); err != nil { + return nil, err + } + if len(response.Releases) == 0 { + return nil, nil + } + version, err := gover.ParseVersionFromRegex(response.Releases[0], javaVersionRegex) + if err != nil { + return nil, err + } + c.releaseNames[version.Raw] = response.Releases[0] + return version, nil +} + +func (c *javaComponent) GetAllVersions() ([]*gover.Version, error) { + archPart, err := installer.Tools.System.MapArchitecture(map[string]string{ + installer.AMD64: "x64", + installer.ARM64: "aarch64", + }) + if err != nil { + return nil, err + } + + allVersions := []*gover.Version{} + pageSize := 100 + for page := 0; ; page++ { + pageUrl := fmt.Sprintf("%s?release_type=ga&os=linux&architecture=%s&image_type=jdk&vendor=eclipse&page=%d&page_size=%d", + c.VersionsUrl, archPart, page, pageSize) + data, err := installer.Tools.Download.AsBytes(pageUrl) + if err != nil { + return nil, err + } + var response struct { + Releases []string `json:"releases"` + } + if err := json.Unmarshal(data, &response); err != nil { + return nil, err + } + if len(response.Releases) == 0 { + break + } + for _, releaseName := range response.Releases { + version, err := gover.ParseVersionFromRegex(releaseName, javaVersionRegex) + if err != nil { + continue // skip e.g. Java 8 releases with a different naming scheme + } + c.releaseNames[version.Raw] = releaseName + allVersions = append(allVersions, version) + } + if len(response.Releases) < pageSize { + break // last page reached + } + } + return allVersions, nil +} + +func (c *javaComponent) InstallVersion(version *gover.Version) error { + archPart, err := installer.Tools.System.MapArchitecture(map[string]string{ + installer.AMD64: "x64", + installer.ARM64: "aarch64", + }) + if err != nil { + return err + } + + releaseName, ok := c.releaseNames[version.Raw] + if !ok { + return fmt.Errorf("could not find release name for Java version %s", version.Raw) + } + + // Build the download URL (url.JoinPath encodes '+' as '%2B' automatically) + downloadUrl, err := installer.Tools.Http.BuildUrl(c.DownloadUrl, + "version", releaseName, "linux", archPart, "jdk", "hotspot", "normal", "eclipse") + if err != nil { + return err + } + + fileName := fmt.Sprintf("java-%s-linux-%s.tar.gz", version.Raw, archPart) + if err := installer.Tools.Download.ToFile(downloadUrl, fileName, "Java"); err != nil { + return err + } + + // Remove old installation and extract to /usr/local/java (strip the root folder) + os.RemoveAll("/usr/local/java") + if err := installer.Tools.Compression.ExtractTarGz(fileName, "/usr/local/java", true); err != nil { + return err + } + + // Cleanup + if err := os.Remove(fileName); err != nil { + return err + } + return nil +} + +////////// +// Maven Component +////////// + +type mavenComponent struct { + *installer.ComponentBase + DownloadUrl string + VersionsUrl string +} + +func (c *mavenComponent) GetAllVersions() ([]*gover.Version, error) { + return installer.Tools.Http.GetVersionsFromHtmlIndex(c.VersionsUrl, mavenIndexLineRegex, mavenVersionRegex) +} + +func (c *mavenComponent) InstallVersion(version *gover.Version) error { + fileName := fmt.Sprintf("apache-maven-%s-bin.tar.gz", version.Raw) + downloadUrl, err := installer.Tools.Http.BuildUrl(c.DownloadUrl, version.Raw, "binaries", fileName) + if err != nil { + return err + } + if err := installer.Tools.Download.ToFile(downloadUrl, fileName, "Maven"); err != nil { + return err + } + + // Remove old installation and extract to /usr/local/maven (strip the root folder) + os.RemoveAll("/usr/local/maven") + if err := installer.Tools.Compression.ExtractTarGz(fileName, "/usr/local/maven", true); err != nil { + return err + } + + // Cleanup + if err := os.Remove(fileName); err != nil { + return err + } + return nil +} + +////////// +// Gradle Component +////////// + +type gradleVersionEntry struct { + Version string `json:"version"` + Nightly bool `json:"nightly"` + Snapshot bool `json:"snapshot"` + ReleaseNightly bool `json:"releaseNightly"` + ActiveRc bool `json:"activeRc"` + Broken bool `json:"broken"` + RcFor string `json:"rcFor"` + MilestoneFor string `json:"milestoneFor"` +} + +type gradleComponent struct { + *installer.ComponentBase + DownloadUrl string + VersionsUrl string +} + +// Always resolve through GetAllVersions to handle mixed 2/3-segment Gradle versions. +func (c *gradleComponent) IsFullVersion(referenceVersion *gover.Version) bool { + return false +} + +func (c *gradleComponent) GetAllVersions() ([]*gover.Version, error) { + data, err := installer.Tools.Download.AsBytes(c.VersionsUrl) + if err != nil { + return nil, err + } + var entries []gradleVersionEntry + if err := json.Unmarshal(data, &entries); err != nil { + return nil, err + } + allVersions := []*gover.Version{} + for _, entry := range entries { + // Only include stable (non-broken, non-pre-release) versions + if entry.Broken || entry.Nightly || entry.Snapshot || entry.ReleaseNightly || entry.ActiveRc || entry.RcFor != "" || entry.MilestoneFor != "" { + continue + } + version, err := gover.ParseVersionFromRegex(entry.Version, gradleVersionRegex) + if err != nil { + continue + } + allVersions = append(allVersions, version) + } + return allVersions, nil +} + +func (c *gradleComponent) InstallVersion(version *gover.Version) error { + fileName := fmt.Sprintf("gradle-%s-bin.zip", version.Raw) + downloadUrl, err := installer.Tools.Http.BuildUrl(c.DownloadUrl, fileName) + if err != nil { + return err + } + if err := installer.Tools.Download.ToFile(downloadUrl, fileName, "Gradle"); err != nil { + return err + } + + // Remove old installation and extract to /usr/local/gradle (strip the root folder) + os.RemoveAll("/usr/local/gradle") + if err := installer.Tools.Compression.ExtractZip(fileName, "/usr/local/gradle", true); err != nil { + return err + } + + // Cleanup + if err := os.Remove(fileName); err != nil { + return err + } + return nil +} + +////////// +// Ant Component +////////// + +type antComponent struct { + *installer.ComponentBase + DownloadUrl string + VersionsUrl string +} + +func (c *antComponent) GetAllVersions() ([]*gover.Version, error) { + return installer.Tools.Http.GetVersionsFromHtmlIndex(c.VersionsUrl, antIndexLineRegex, antVersionRegex) +} + +func (c *antComponent) InstallVersion(version *gover.Version) error { + fileName := fmt.Sprintf("apache-ant-%s-bin.tar.gz", version.Raw) + downloadUrl, err := installer.Tools.Http.BuildUrl(c.DownloadUrl, fileName) + if err != nil { + return err + } + if err := installer.Tools.Download.ToFile(downloadUrl, fileName, "Ant"); err != nil { + return err + } + + // Remove old installation and extract to /usr/local/ant (strip the root folder) + os.RemoveAll("/usr/local/ant") + if err := installer.Tools.Compression.ExtractTarGz(fileName, "/usr/local/ant", true); err != nil { + return err + } + + // Cleanup + if err := os.Remove(fileName); err != nil { + return err + } + return nil +} diff --git a/features/test/java/install-exact.sh b/features/test/java/install-exact.sh new file mode 100644 index 0000000..6f85b25 --- /dev/null +++ b/features/test/java/install-exact.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_command_exists java +check_env_var_exists JAVA_HOME +check_version "$(java -version 2>&1)" "21.0.5" diff --git a/features/test/java/install-latest.sh b/features/test/java/install-latest.sh new file mode 100644 index 0000000..2b08270 --- /dev/null +++ b/features/test/java/install-latest.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_command_exists java +check_env_var_exists JAVA_HOME +check_version "$(java -version 2>&1)" "openjdk" diff --git a/features/test/java/install-with-ant.sh b/features/test/java/install-with-ant.sh new file mode 100644 index 0000000..a37e554 --- /dev/null +++ b/features/test/java/install-with-ant.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_command_exists java +check_command_exists ant +check_env_var_exists JAVA_HOME +check_env_var_exists ANT_HOME +check_version "$(java -version 2>&1)" "openjdk" +check_version "$(ant -version)" "Apache Ant" diff --git a/features/test/java/install-with-gradle.sh b/features/test/java/install-with-gradle.sh new file mode 100644 index 0000000..ea19967 --- /dev/null +++ b/features/test/java/install-with-gradle.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_command_exists java +check_command_exists gradle +check_env_var_exists JAVA_HOME +check_env_var_exists GRADLE_HOME +check_version "$(java -version 2>&1)" "openjdk" +check_version "$(gradle --version)" "Gradle" diff --git a/features/test/java/install-with-maven.sh b/features/test/java/install-with-maven.sh new file mode 100644 index 0000000..3eebd40 --- /dev/null +++ b/features/test/java/install-with-maven.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_command_exists java +check_command_exists mvn +check_env_var_exists JAVA_HOME +check_env_var_exists MAVEN_HOME +check_version "$(java -version 2>&1)" "openjdk" +check_version "$(mvn --version)" "Apache Maven" diff --git a/features/test/java/install-with-resolve.sh b/features/test/java/install-with-resolve.sh new file mode 100644 index 0000000..8cd39a4 --- /dev/null +++ b/features/test/java/install-with-resolve.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_command_exists java +check_env_var_exists JAVA_HOME +check_version "$(java -version 2>&1)" "21." diff --git a/features/test/java/scenarios.json b/features/test/java/scenarios.json new file mode 100644 index 0000000..455b994 --- /dev/null +++ b/features/test/java/scenarios.json @@ -0,0 +1,83 @@ +{ + "install-latest": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./java": { + "version": "latest" + } + } + }, + "install-exact": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./java": { + "version": "21.0.5" + } + } + }, + "install-with-maven": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./java": { + "version": "21", + "mavenVersion": "latest" + } + } + }, + "install-with-gradle": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./java": { + "version": "21", + "gradleVersion": "latest" + } + } + }, + "install-with-resolve": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./java": { + "version": "21" + } + } + }, + "install-with-ant": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./java": { + "version": "21", + "antVersion": "latest" + } + } + } +} diff --git a/features/test/java/test-images.json b/features/test/java/test-images.json new file mode 100644 index 0000000..e7aa820 --- /dev/null +++ b/features/test/java/test-images.json @@ -0,0 +1,4 @@ +[ + "mcr.microsoft.com/devcontainers/base:debian-12", + "mcr.microsoft.com/devcontainers/base:ubuntu-24.04" +] diff --git a/override-all.env b/override-all.env index 78e5f07..77b1d6e 100644 --- a/override-all.env +++ b/override-all.env @@ -45,6 +45,17 @@ GO_DOWNLOAD_URL="" GO_LATEST_URL="" GO_VERSIONS_URL="" +# java +JAVA_DOWNLOAD_URL="" +JAVA_VERSIONS_URL="" +JAVA_LATEST_URL="" +JAVA_MAVEN_DOWNLOAD_URL="" +JAVA_MAVEN_VERSIONS_URL="" +JAVA_GRADLE_DOWNLOAD_URL="" +JAVA_GRADLE_VERSIONS_URL="" +JAVA_ANT_DOWNLOAD_URL="" +JAVA_ANT_VERSIONS_URL="" + # gonovate GONOVATE_DOWNLOAD_URL="" From 1f1fd9248cf2a7d0b34a7746a401d813cc6068d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:24:05 +0000 Subject: [PATCH 3/3] Fix containerEnv to only set JAVA_HOME and use symlinks for Maven/Gradle/Ant Agent-Logs-Url: https://github.com/postfinance/devcontainer-features/sessions/5629081a-8a0a-49d2-8806-eb80887989b9 Co-authored-by: Roemer <393641+Roemer@users.noreply.github.com> --- features/src/java/devcontainer-feature.json | 5 +---- features/src/java/installer.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/features/src/java/devcontainer-feature.json b/features/src/java/devcontainer-feature.json index 0b539c1..e5f53e4 100644 --- a/features/src/java/devcontainer-feature.json +++ b/features/src/java/devcontainer-feature.json @@ -116,9 +116,6 @@ }, "containerEnv": { "JAVA_HOME": "/usr/local/java", - "MAVEN_HOME": "/usr/local/maven", - "GRADLE_HOME": "/usr/local/gradle", - "ANT_HOME": "/usr/local/ant", - "PATH": "/usr/local/java/bin:/usr/local/maven/bin:/usr/local/gradle/bin:/usr/local/ant/bin:${PATH}" + "PATH": "/usr/local/java/bin:${PATH}" } } diff --git a/features/src/java/installer.go b/features/src/java/installer.go index 6a2e8bb..b676968 100644 --- a/features/src/java/installer.go +++ b/features/src/java/installer.go @@ -279,6 +279,11 @@ func (c *mavenComponent) InstallVersion(version *gover.Version) error { return err } + // Create a symlink in /usr/local/bin so mvn is available without changing PATH + if err := installer.Tools.FileSystem.CreateSymLink("/usr/local/maven/bin/mvn", "/usr/local/bin/mvn", false); err != nil { + return err + } + // Cleanup if err := os.Remove(fileName); err != nil { return err @@ -352,6 +357,11 @@ func (c *gradleComponent) InstallVersion(version *gover.Version) error { return err } + // Create a symlink in /usr/local/bin so gradle is available without changing PATH + if err := installer.Tools.FileSystem.CreateSymLink("/usr/local/gradle/bin/gradle", "/usr/local/bin/gradle", false); err != nil { + return err + } + // Cleanup if err := os.Remove(fileName); err != nil { return err @@ -389,6 +399,11 @@ func (c *antComponent) InstallVersion(version *gover.Version) error { return err } + // Create a symlink in /usr/local/bin so ant is available without changing PATH + if err := installer.Tools.FileSystem.CreateSymLink("/usr/local/ant/bin/ant", "/usr/local/bin/ant", false); err != nil { + return err + } + // Cleanup if err := os.Remove(fileName); err != nil { return err