From 2c473c0300a139872b482027c7e96f5de405b3f3 Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Tue, 24 Mar 2026 13:02:37 +0000 Subject: [PATCH 1/7] feat(dotnet): add dotnet feature --- README.md | 1 + build/build.go | 5 + features/src/dotnet/NOTES.md | 33 ++ features/src/dotnet/README.md | 73 ++++ features/src/dotnet/devcontainer-feature.json | 89 ++++ features/src/dotnet/install.sh | 11 + features/src/dotnet/installer.go | 387 ++++++++++++++++++ features/test/dotnet/multiple-sdk.sh | 15 + features/test/dotnet/scenarios.json | 32 ++ features/test/dotnet/sdk-only.sh | 10 + features/test/dotnet/test-images.json | 5 + override-all.env | 5 + 12 files changed, 666 insertions(+) create mode 100644 features/src/dotnet/NOTES.md create mode 100755 features/src/dotnet/README.md create mode 100644 features/src/dotnet/devcontainer-feature.json create mode 100755 features/src/dotnet/install.sh create mode 100644 features/src/dotnet/installer.go create mode 100755 features/test/dotnet/multiple-sdk.sh create mode 100644 features/test/dotnet/scenarios.json create mode 100755 features/test/dotnet/sdk-only.sh create mode 100644 features/test/dotnet/test-images.json diff --git a/README.md b/README.md index a964ddb..779df87 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Below is a list with included features, click on the link for more details. | [build-essential](./features/src/build-essential/README.md) | Installs build essentials like gcc. | | [cypress-deps](./features/src/cypress-deps/README.md) | Installs all dependencies required to run Cypress. | | [docker-out](./features/src/docker-out/README.md) | Installs a Docker client which re-uses the host Docker socket. | +| [dotnet](./features/src/dotnet/README.md) | A package which installs .NET SDKs, runtimes and workloads. | | [eclipse-deps](./features/src/eclipse-deps/README.md) | Installs all dependencies required to run the Eclipse IDE. | | [git-lfs](./features/src/git-lfs/README.md) | Installs Git LFS. | | [gitlab-cli](./features/src/gitlab-cli/README.md) | Installs the GitLab CLI. | diff --git a/build/build.go b/build/build.go index d5157f1..442f5d6 100644 --- a/build/build.go +++ b/build/build.go @@ -100,6 +100,11 @@ func init() { gotaskr.Task("Feature:docker-out:Test", func() error { return testFeature("docker-out") }) gotaskr.Task("Feature:docker-out:Publish", func() error { return publishFeature("docker-out") }) + ////////// dotnet + gotaskr.Task("Feature:dotnet:Package", func() error { return packageFeature("dotnet") }) + gotaskr.Task("Feature:dotnet:Test", func() error { return testFeature("dotnet") }) + gotaskr.Task("Feature:dotnet:Publish", func() error { return publishFeature("dotnet") }) + ////////// eclipse-deps gotaskr.Task("Feature:eclipse-deps:Package", func() error { return packageFeature("eclipse-deps") }) gotaskr.Task("Feature:eclipse-deps:Test", func() error { return testFeature("eclipse-deps") }) diff --git a/features/src/dotnet/NOTES.md b/features/src/dotnet/NOTES.md new file mode 100644 index 0000000..e627ae8 --- /dev/null +++ b/features/src/dotnet/NOTES.md @@ -0,0 +1,33 @@ +## Dotnet Tools + +If you need additional tools for example like the Powerapps CLI you can install them using `dotnet tool install --create-manifest-if-needed `. +This installs the tool creates a manifest file: `.config/dotnet-tools.json`. + +```json +{ + "version": 1, + "isRoot": true, + "tools": { + "microsoft.powerapps.cli.tool": { + "version": "1.43.6", + "commands": [ + "pac" + ], + "rollForward": false + } + } +} +``` + +After this step, the tool can be invoked using `dotnet `. + +If you already have a manifest, all tools can be installed using `dotnet tool restore`. + +To do that automatically, include the command in your `devcontainer.json` like this: +```json +"postCreateCommand": "dotnet tool restore" +``` + +### System Compatibility + +Debian, Ubuntu, Alpine diff --git a/features/src/dotnet/README.md b/features/src/dotnet/README.md new file mode 100755 index 0000000..e77e5ae --- /dev/null +++ b/features/src/dotnet/README.md @@ -0,0 +1,73 @@ +# .NET (dotnet) + +A package which installs .NET SDKs, runtimes and workloads. + +## Example Usage + +```json +"features": { + "ghcr.io/postfinance/devcontainer-features/dotnet:0.1.0": { + "version": "lts", + "additionalVersions": "", + "dotnetRuntimeVersions": "", + "aspNetCoreRuntimeVersions": "", + "workloads": "", + "downloadUrl": "", + "versionsUrl": "", + "nugetConfigPath": "" + } +} +``` + +## Options + +| Option | Description | Type | Default Value | Proposals | +|-----|-----|-----|-----|-----| +| version | Select or enter a .NET SDK version. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | lts | latest, lts, none, 8.0, 7.0, 6.0, 8.0.408 | +| additionalVersions | Enter additional .NET SDK versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 7.0,8.0, 8.0.408 | +| dotnetRuntimeVersions | Enter additional .NET runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 8.0.15, 9.0, lts, 7.0 | +| aspNetCoreRuntimeVersions | Enter additional ASP.NET Core runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 8.0.15, lts, 7.0 | +| workloads | Enter additional .NET SDK workloads, separated by commas. Use 'dotnet workload search' to learn what workloads are available to install. | string | <empty> | wasm-tools, android, macos | +| downloadUrl | The download URL to use for Dotnet binaries. | string | <empty> | | +| versionsUrl | The URL to use for fetching available Dotnet versions. | string | <empty> | | +| nugetConfigPath | Path to a NuGet.Config file to copy into the container. This can be used to configure private package sources for the dotnet CLI. | string | <empty> | | + +## Customizations + +### VS Code Extensions + +- `ms-dotnettools.csharp` + +## Dotnet Tools + +If you need additional tools for example like the Powerapps CLI you can install them using `dotnet tool install --create-manifest-if-needed `. +This installs the tool creates a manifest file: `.config/dotnet-tools.json`. + +```json +{ + "version": 1, + "isRoot": true, + "tools": { + "microsoft.powerapps.cli.tool": { + "version": "1.43.6", + "commands": [ + "pac" + ], + "rollForward": false + } + } +} +``` + +After this step, the tool can be invoked using `dotnet `. + +If you already have a manifest, all tools can be installed using `dotnet tool restore`. + +To do that automatically, include the command in your `devcontainer.json` like this: +```json +"postCreateCommand": "dotnet tool restore" +``` + +### System Compatibility + +Debian, Ubuntu, Alpine diff --git a/features/src/dotnet/devcontainer-feature.json b/features/src/dotnet/devcontainer-feature.json new file mode 100644 index 0000000..083facc --- /dev/null +++ b/features/src/dotnet/devcontainer-feature.json @@ -0,0 +1,89 @@ +{ + "id": "dotnet", + "version": "0.1.0", + "name": ".NET", + "description": "A package which installs .NET SDKs, runtimes and workloads.", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest", + "lts", + "none", + "8.0", + "7.0", + "6.0", + "8.0.408" + ], + "default": "lts", + "description": "Select or enter a .NET SDK version. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version." + }, + "additionalVersions": { + "type": "string", + "default": "", + "description": "Enter additional .NET SDK versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version.", + "proposals": [ + "7.0,8.0", + "8.0.408" + ] + }, + "dotnetRuntimeVersions": { + "type": "string", + "default": "", + "description": "Enter additional .NET runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version.", + "proposals": [ + "8.0.15", + "9.0", + "lts, 7.0" + ] + }, + "aspNetCoreRuntimeVersions": { + "type": "string", + "default": "", + "description": "Enter additional ASP.NET Core runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version.", + "proposals": [ + "8.0.15", + "lts, 7.0" + ] + }, + "workloads": { + "type": "string", + "default": "", + "description": "Enter additional .NET SDK workloads, separated by commas. Use 'dotnet workload search' to learn what workloads are available to install.", + "proposals": [ + "wasm-tools", + "android, macos" + ] + }, + "downloadUrl": { + "type": "string", + "default": "", + "description": "The download URL to use for Dotnet binaries." + }, + "versionsUrl": { + "type": "string", + "default": "", + "description": "The URL to use for fetching available Dotnet versions." + }, + "nugetConfigPath": { + "type": "string", + "default": "", + "description": "Path to a NuGet.Config file to copy into the container. This can be used to configure private package sources for the dotnet CLI." + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp" + ] + } + }, + "containerEnv": { + "DOTNET_ROOT": "/usr/share/dotnet", + "PATH": "$PATH:$DOTNET_ROOT:~/.dotnet/tools", + "DOTNET_RUNNING_IN_CONTAINER": "true", + "DOTNET_USE_POLLING_FILE_WATCHER": "true", + "DOTNET_CLI_TELEMETRY_OPTOUT": "1" + }, + "onCreateCommand": "dotnet nuget remove source nuget.org" +} \ No newline at end of file diff --git a/features/src/dotnet/install.sh b/features/src/dotnet/install.sh new file mode 100755 index 0000000..61410bd --- /dev/null +++ b/features/src/dotnet/install.sh @@ -0,0 +1,11 @@ +. ./functions.sh + +"./installer_$(detect_arch)" \ +-version="${VERSION:-"latest"}" \ +-additionalVersions="${ADDITIONALVERSIONS:-""}" \ +-dotnetRuntimeVersions="${DOTNETRUNTIMEVERSIONS:-""}" \ +-aspNetCoreRuntimeVersions="${ASPNETCORERUNTIMEVERSIONS:-""}" \ +-workloads="${WORKLOADS:-""}" \ +-downloadUrl="${DOWNLOADURL:-""}" \ +-versionsUrl="${VERSIONSURL:-""}" \ +-nugetConfigPath="${NUGETCONFIGPATH:-""}" \ diff --git a/features/src/dotnet/installer.go b/features/src/dotnet/installer.go new file mode 100644 index 0000000..f6d3eec --- /dev/null +++ b/features/src/dotnet/installer.go @@ -0,0 +1,387 @@ +package main + +import ( + "builder/installer" + "flag" + "fmt" + "os" + "regexp" + "strings" + + "github.com/roemer/gotaskr/execr" + "github.com/roemer/gover" +) + +////////// +// Configuration +////////// + +type Product int + +const ( + sdk Product = iota + runtime + aspNetRuntime +) + +func (c Product) String() string { + switch c { + case sdk: + return "Sdk" + case runtime: + return "Runtime" + case aspNetRuntime: + return "aspnetcore/Runtime" + default: + return "" + } +} + +type DotnetVersion struct { + Releases []struct { + Runtime DotnetProduct `json:"runtime"` + Sdks []DotnetProduct `json:"sdks"` + AspnetcoreRuntime DotnetProduct `json:"aspnetcore-runtime"` + } `json:"releases"` +} + +type DotnetProduct struct { + Version string `json:"version"` + Files []File `json:"files"` +} + +type File struct { + Name string `json:"name"` + Rid string `json:"rid"` + URL string `json:"url"` + Hash string `json:"hash"` +} + +////////// +// 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", "") + additionalVersions := flag.String("additionalVersions", "", "") + dotnetRuntimeVersions := flag.String("dotnetRuntimeVersions", "", "") + aspNetCoreRuntimeVersions := flag.String("aspNetCoreRuntimeVersions", "", "") + workloads := flag.String("workloads", "", "") + downloadUrl := flag.String("downloadUrl", "", "") + versionsUrl := flag.String("versionsUrl", "", "") + nugetConfigPath := flag.String("nugetConfigPath", "", "") + + flag.Parse() + + // Load settings from an external file + if err := installer.LoadOverrides(); err != nil { + return err + } + + installer.HandleOverride(downloadUrl, "https://builds.dotnet.microsoft.com/dotnet", "dotnet-download-url") + installer.HandleOverride(versionsUrl, "https://builds.dotnet.microsoft.com/dotnet", "dotnet-versions-url") + installer.HandleOverride(nugetConfigPath, "", "dotnet-nuget-config-path") + + // Handle multi value fields + var allSdks = []string{*version} + if len(*additionalVersions) > 0 { + allSdks = append(allSdks, strings.Split(*additionalVersions, ",")...) + } + var additionalRuntimes = []string{} + if len(*dotnetRuntimeVersions) > 0 { + additionalRuntimes = strings.Split(*dotnetRuntimeVersions, ",") + } + var additionalaspNetCoreRuntimes = []string{} + if len(*aspNetCoreRuntimeVersions) > 0 { + additionalaspNetCoreRuntimes = strings.Split(*aspNetCoreRuntimeVersions, ",") + } + // Create the feature + feature := installer.NewFeature(".NET", true) + if *nugetConfigPath != "" { + feature.AddComponents(&nugetConfigComponent{ + ComponentBase: installer.NewComponentBase("Nuget Config", installer.VERSION_IRRELEVANT), + NugetConfigPath: *nugetConfigPath, + }) + } + // add sdks + for _, sdkVersion := range allSdks { + component := &sdkComponent{ + ComponentBase: installer.NewComponentBase(fmt.Sprintf("SDK [%s]", sdkVersion), strings.TrimSpace(sdkVersion)), + DownloadUrl: *downloadUrl, + VersionsUrl: *versionsUrl, + } + feature.AddComponents(component) + } + // add runtimes + for _, runtimeVersion := range additionalRuntimes { + component := &runtimeComponent{ + ComponentBase: installer.NewComponentBase(fmt.Sprintf("Runtime [%s]", runtimeVersion), strings.TrimSpace(runtimeVersion)), + DownloadUrl: *downloadUrl, + VersionsUrl: *versionsUrl, + } + feature.AddComponents(component) + } + // add runtimes + for _, aspCoreVersion := range additionalaspNetCoreRuntimes { + component := &aspNetRuntimeComponent{ + ComponentBase: installer.NewComponentBase(fmt.Sprintf("ASP.NET Core runtime [%s]", aspCoreVersion), strings.TrimSpace(aspCoreVersion)), + DownloadUrl: *downloadUrl, + VersionsUrl: *versionsUrl, + } + feature.AddComponents(component) + } + // workloads + if len(*workloads) > 0 { + feature.AddComponents(&workloadComponent{ + ComponentBase: installer.NewComponentBase("Workloads", installer.VERSION_IRRELEVANT), + workloads: strings.Split(strings.ReplaceAll(*workloads, " ", ""), ","), + }) + } + // Component to create a symlink, but only if an sdk was installed + if len(allSdks) > 0 { + feature.AddComponents(&symlinkComponent{ + ComponentBase: installer.NewComponentBase("SymLink", installer.VERSION_IRRELEVANT), + }) + } + // Process the feature + return feature.Process() +} + +////////// +// Implementation +////////// + +type sdkComponent struct { + *installer.ComponentBase + DownloadUrl string + VersionsUrl string +} + +func (c *sdkComponent) GetAllVersions() ([]*gover.Version, error) { + latestVersion, err := resolveSdkVersion(c.VersionsUrl, c.GetRequestedVersion()) + if err != nil { + return nil, err + } + version, err := gover.ParseVersionFromRegex(latestVersion, gover.RegexpSemver) + if err != nil { + return nil, err + } + return []*gover.Version{version}, err +} + +func (c *sdkComponent) GetLatestVersion() (*gover.Version, error) { + latestVersion, err := resolveSdkVersion(c.VersionsUrl, installer.VERSION_LTS) + if err != nil { + return nil, err + } + version, err := gover.ParseVersionFromRegex(latestVersion, gover.RegexpSemver) + if err != nil { + return nil, err + } + return version, err +} + +func (c *sdkComponent) InstallVersion(version *gover.Version) error { + return installSdk(c.DownloadUrl, version) +} + +type runtimeComponent struct { + *installer.ComponentBase + DownloadUrl string + VersionsUrl string +} + +func (c *runtimeComponent) GetAllVersions() ([]*gover.Version, error) { + latestVersion, err := resolveRuntimeVersion(c.VersionsUrl, c.GetRequestedVersion()) + if err != nil { + return nil, err + } + version, err := gover.ParseVersionFromRegex(latestVersion, gover.RegexpSemver) + if err != nil { + return nil, err + } + return []*gover.Version{version}, err +} + +func (c *runtimeComponent) GetLatestVersion() (*gover.Version, error) { + latestVersion, err := resolveRuntimeVersion(c.VersionsUrl, installer.VERSION_LTS) + if err != nil { + return nil, err + } + version, err := gover.ParseVersionFromRegex(latestVersion, gover.RegexpSemver) + if err != nil { + return nil, err + } + return version, err +} + +func (c *runtimeComponent) InstallVersion(version *gover.Version) error { + return installRuntime(c.DownloadUrl, version) +} + +type aspNetRuntimeComponent struct { + *installer.ComponentBase + DownloadUrl string + VersionsUrl string +} + +func (c *aspNetRuntimeComponent) GetAllVersions() ([]*gover.Version, error) { + latestVersion, err := resolveAspNetRuntimeVersion(c.VersionsUrl, c.GetRequestedVersion()) + if err != nil { + return nil, err + } + version, err := gover.ParseVersionFromRegex(latestVersion, gover.RegexpSemver) + if err != nil { + return nil, err + } + return []*gover.Version{version}, err +} + +func (c *aspNetRuntimeComponent) GetLatestVersion() (*gover.Version, error) { + latestVersion, err := resolveAspNetRuntimeVersion(c.VersionsUrl, installer.VERSION_LTS) + if err != nil { + return nil, err + } + version, err := gover.ParseVersionFromRegex(latestVersion, gover.RegexpSemver) + if err != nil { + return nil, err + } + return version, err +} + +func (c *aspNetRuntimeComponent) InstallVersion(version *gover.Version) error { + return installAspNetRuntime(c.DownloadUrl, version) +} + +func installSdk(downloadUrl string, version *gover.Version) error { + return installDotnetBinary(downloadUrl, sdk, "dotnet-sdk", "downloading sdk", version) +} + +func installRuntime(downloadUrl string, version *gover.Version) error { + return installDotnetBinary(downloadUrl, runtime, "dotnet-runtime", "downloading runtime", version) +} + +func installAspNetRuntime(downloadUrl string, version *gover.Version) error { + return installDotnetBinary(downloadUrl, aspNetRuntime, "aspnetcore-runtime", "downloading ASP.NET runtime", version) +} + +func installDotnetBinary(downloadUrl string, product Product, fileName string, progressName string, version *gover.Version) error { + osInfo, err := installer.Tools.System.GetOsInfo() + if err != nil { + return err + } + + // Determine the architecture part of the url + archPart, err := installer.Tools.System.MapArchitecture(map[string]string{ + installer.AMD64: "x64", + installer.ARM64: "arm64", + }) + if err != nil { + return err + } + arch := "" + if osInfo.IsAlpine() { + arch = fmt.Sprintf("linux-musl-%s", archPart) + installer.Tools.System.InstallPackages("ca-certificates", "libgcc", "libssl3", "libstdc++", "zlib", "icu-libs", "icu-data-full", "tzdata", "krb5") + } else if osInfo.IsDebian() || osInfo.IsUbuntu() { + arch = fmt.Sprintf("linux-%s", archPart) + } else { + return fmt.Errorf("unsupported OS for .NET install") + } + + // Download file + downloadedFileName := fmt.Sprintf("%s-%s-%s.tar.gz", fileName, version.Raw, arch) + fullUrl := fmt.Sprintf("%s/%s/%s/%s", downloadUrl, product, version.Raw, downloadedFileName) + if err := installer.Tools.Download.ToFile(fullUrl, downloadedFileName, progressName); err != nil { + return err + } + + // Extract it + if err := installer.Tools.Compression.ExtractTarGz(downloadedFileName, os.Getenv("DOTNET_ROOT"), false); err != nil { + return err + } + // Cleanup + if err := os.Remove(downloadedFileName); err != nil { + return err + } + return nil +} + +func resolveSdkVersion(versionsUrl string, requestedVersion string) (string, error) { + return resolveVersion(versionsUrl, requestedVersion, sdk) +} + +func resolveRuntimeVersion(versionsUrl string, requestedVersion string) (string, error) { + return resolveVersion(versionsUrl, requestedVersion, runtime) +} + +func resolveAspNetRuntimeVersion(versionsUrl string, requestedVersion string) (string, error) { + return resolveVersion(versionsUrl, requestedVersion, aspNetRuntime) +} + +func resolveVersion(versionsUrl string, requestedVersion string, product Product) (string, error) { + var latestVersion string + regexChannel := regexp.MustCompile(`^(sts|lts|\d+\.\d+)$`) + if regexChannel.MatchString(strings.ToLower(requestedVersion)) { + // 4.0 works as channel + // 8.0.1xx feature band should work according to docs but does somehow work only for some versions, e.g. 8.0.2xx does not work... + latestVersionUrl := fmt.Sprintf("%s/%s/%s/latest.version", versionsUrl, product, strings.ToUpper(requestedVersion)) + + version, err := installer.Tools.Download.AsString(latestVersionUrl) + if err != nil { + return "", err + } + latestVersion = version + } else { + latestVersion = requestedVersion + } + return latestVersion, nil +} + +type workloadComponent struct { + *installer.ComponentBase + workloads []string +} + +func (c *workloadComponent) InstallVersion(version *gover.Version) error { + arguments := append([]string{"workload", "install", "--temp-dir", "/tmp/dotnet-workload-temp-dir"}, c.workloads...) + if err := execr.Run(true, "dotnet", arguments...); err != nil { + return err + } + // # Clean up + return os.RemoveAll("/tmp/dotnet-workload-temp-dir") +} + +type symlinkComponent struct { + *installer.ComponentBase +} + +func (c *symlinkComponent) InstallVersion(version *gover.Version) error { + return installer.Tools.FileSystem.CreateSymLink(fmt.Sprintf("%s/dotnet", os.Getenv("DOTNET_ROOT")), "/usr/bin/dotnet", false) +} + +type nugetConfigComponent struct { + *installer.ComponentBase + NugetConfigPath string +} + +func (c *nugetConfigComponent) InstallVersion(version *gover.Version) error { + fileContent, err := installer.ReadFileFromUrlOrLocal(c.NugetConfigPath) + if err != nil { + return err + } + // ensure nuget.org source is disabled and we only access sources defined in the provided config file + execr.Run(true, "dotnet", "nuget", "disable", "source", "nuget.org") + if err := os.MkdirAll("/etc/opt/NuGet", 0755); err != nil { + return err + } + return os.WriteFile("/etc/opt/NuGet/NuGetDefaults.config", fileContent, 0644) +} diff --git a/features/test/dotnet/multiple-sdk.sh b/features/test/dotnet/multiple-sdk.sh new file mode 100755 index 0000000..4f4c567 --- /dev/null +++ b/features/test/dotnet/multiple-sdk.sh @@ -0,0 +1,15 @@ +#!/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 "dotnet" +check_file_exists /usr/bin/dotnet +check_command_exists /usr/bin/dotnet +check_version "$(dotnet --list-sdks)" "8.0." +check_version "$(dotnet --list-sdks)" "9.0." +check_version "$(dotnet --list-runtimes)" "NETCore.App 6.0." +check_version "$(dotnet --list-runtimes)" "AspNetCore.App 8.0.15" +check_version "$(dotnet workload list)" "wasm-tools-net7" +check_version "$(dotnet workload list)" "maui-android" diff --git a/features/test/dotnet/scenarios.json b/features/test/dotnet/scenarios.json new file mode 100644 index 0000000..6d0678d --- /dev/null +++ b/features/test/dotnet/scenarios.json @@ -0,0 +1,32 @@ +{ + "sdk-only": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./dotnet": { + "version": "8.0" + } + } + }, + "multiple-sdk": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./dotnet": { + "version": "8.0", + "additionalVersions": "9.0.102", + "dotnetRuntimeVersions": "6.0", + "aspNetCoreRuntimeVersions": "8.0.15", + "workloads": "maui-android, wasm-tools-net7" + } + } + } +} \ No newline at end of file diff --git a/features/test/dotnet/sdk-only.sh b/features/test/dotnet/sdk-only.sh new file mode 100755 index 0000000..3e0e347 --- /dev/null +++ b/features/test/dotnet/sdk-only.sh @@ -0,0 +1,10 @@ +#!/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 "dotnet" +check_version "$(dotnet --version | head -1)" "8.0." +check_file_exists /usr/bin/dotnet +check_command_exists /usr/bin/dotnet diff --git a/features/test/dotnet/test-images.json b/features/test/dotnet/test-images.json new file mode 100644 index 0000000..12913d7 --- /dev/null +++ b/features/test/dotnet/test-images.json @@ -0,0 +1,5 @@ +[ + "mcr.microsoft.com/devcontainers/base:debian-12", + "mcr.microsoft.com/devcontainers/base:alpine", + "mcr.microsoft.com/devcontainers/base:ubuntu-24.04" +] \ No newline at end of file diff --git a/override-all.env b/override-all.env index a8d5c61..284649e 100644 --- a/override-all.env +++ b/override-all.env @@ -19,6 +19,11 @@ DOCKER_OUT_COMPOSE_DOWNLOAD_URL="" DOCKER_OUT_BUILDX_DOWNLOAD_URL="" DOCKER_OUT_CONFIG_PATH="" +# dotnet +DOTNET_DOWNLOAD_URL="" +DOTNET_VERSIONS_URL="" +DOTNET_NUGET_CONFIG_PATH="" + # git-lfs GIT_LFS_DOWNLOAD_URL="" From fcd40e030bfc5ad76e402e01d09d0b7828cdcea1 Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Tue, 24 Mar 2026 15:34:39 +0000 Subject: [PATCH 2/7] chore: fix tests for arm --- features/test/dotnet/multiple-sdk.sh | 2 +- features/test/dotnet/scenarios.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/test/dotnet/multiple-sdk.sh b/features/test/dotnet/multiple-sdk.sh index 4f4c567..25d03f1 100755 --- a/features/test/dotnet/multiple-sdk.sh +++ b/features/test/dotnet/multiple-sdk.sh @@ -11,5 +11,5 @@ check_version "$(dotnet --list-sdks)" "8.0." check_version "$(dotnet --list-sdks)" "9.0." check_version "$(dotnet --list-runtimes)" "NETCore.App 6.0." check_version "$(dotnet --list-runtimes)" "AspNetCore.App 8.0.15" -check_version "$(dotnet workload list)" "wasm-tools-net7" +check_version "$(dotnet workload list)" "wasm-tools-net8" check_version "$(dotnet workload list)" "maui-android" diff --git a/features/test/dotnet/scenarios.json b/features/test/dotnet/scenarios.json index 6d0678d..d8ea22d 100644 --- a/features/test/dotnet/scenarios.json +++ b/features/test/dotnet/scenarios.json @@ -25,7 +25,7 @@ "additionalVersions": "9.0.102", "dotnetRuntimeVersions": "6.0", "aspNetCoreRuntimeVersions": "8.0.15", - "workloads": "maui-android, wasm-tools-net7" + "workloads": "maui-android, wasm-tools-net8" } } } From c858b33d697bf317933455ea2a191915c02d0782 Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Tue, 24 Mar 2026 15:39:30 +0000 Subject: [PATCH 3/7] feat(dotnet): update default .NET SDK version to 10.0 and adjust proposals --- features/src/dotnet/README.md | 10 +++++----- features/src/dotnet/devcontainer-feature.json | 11 +++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/features/src/dotnet/README.md b/features/src/dotnet/README.md index e77e5ae..5d0499a 100755 --- a/features/src/dotnet/README.md +++ b/features/src/dotnet/README.md @@ -7,7 +7,7 @@ A package which installs .NET SDKs, runtimes and workloads. ```json "features": { "ghcr.io/postfinance/devcontainer-features/dotnet:0.1.0": { - "version": "lts", + "version": "10.0", "additionalVersions": "", "dotnetRuntimeVersions": "", "aspNetCoreRuntimeVersions": "", @@ -23,10 +23,10 @@ A package which installs .NET SDKs, runtimes and workloads. | Option | Description | Type | Default Value | Proposals | |-----|-----|-----|-----|-----| -| version | Select or enter a .NET SDK version. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | lts | latest, lts, none, 8.0, 7.0, 6.0, 8.0.408 | -| additionalVersions | Enter additional .NET SDK versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 7.0,8.0, 8.0.408 | -| dotnetRuntimeVersions | Enter additional .NET runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 8.0.15, 9.0, lts, 7.0 | -| aspNetCoreRuntimeVersions | Enter additional ASP.NET Core runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 8.0.15, lts, 7.0 | +| version | Select or enter a .NET SDK version. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | 10.0 | latest, lts, none, 8.0, 9.0, 8.0.408 | +| additionalVersions | Enter additional .NET SDK versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 8.0,9.0, 8.0.408 | +| dotnetRuntimeVersions | Enter additional .NET runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 8.0.15, 9.0, lts, 8.0 | +| aspNetCoreRuntimeVersions | Enter additional ASP.NET Core runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 8.0.15, lts, 8.0 | | workloads | Enter additional .NET SDK workloads, separated by commas. Use 'dotnet workload search' to learn what workloads are available to install. | string | <empty> | wasm-tools, android, macos | | downloadUrl | The download URL to use for Dotnet binaries. | string | <empty> | | | versionsUrl | The URL to use for fetching available Dotnet versions. | string | <empty> | | diff --git a/features/src/dotnet/devcontainer-feature.json b/features/src/dotnet/devcontainer-feature.json index 083facc..807df8d 100644 --- a/features/src/dotnet/devcontainer-feature.json +++ b/features/src/dotnet/devcontainer-feature.json @@ -11,11 +11,10 @@ "lts", "none", "8.0", - "7.0", - "6.0", + "9.0", "8.0.408" ], - "default": "lts", + "default": "10.0", "description": "Select or enter a .NET SDK version. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version." }, "additionalVersions": { @@ -23,7 +22,7 @@ "default": "", "description": "Enter additional .NET SDK versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version.", "proposals": [ - "7.0,8.0", + "8.0,9.0", "8.0.408" ] }, @@ -34,7 +33,7 @@ "proposals": [ "8.0.15", "9.0", - "lts, 7.0" + "lts, 8.0" ] }, "aspNetCoreRuntimeVersions": { @@ -43,7 +42,7 @@ "description": "Enter additional ASP.NET Core runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version.", "proposals": [ "8.0.15", - "lts, 7.0" + "lts, 8.0" ] }, "workloads": { From 08a9a1227b36b4321f6f89d095f9eb1071bf10df Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Tue, 24 Mar 2026 15:50:28 +0000 Subject: [PATCH 4/7] chore: cleanup --- features/src/dotnet/installer.go | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/features/src/dotnet/installer.go b/features/src/dotnet/installer.go index f6d3eec..440ad13 100644 --- a/features/src/dotnet/installer.go +++ b/features/src/dotnet/installer.go @@ -37,26 +37,6 @@ func (c Product) String() string { } } -type DotnetVersion struct { - Releases []struct { - Runtime DotnetProduct `json:"runtime"` - Sdks []DotnetProduct `json:"sdks"` - AspnetcoreRuntime DotnetProduct `json:"aspnetcore-runtime"` - } `json:"releases"` -} - -type DotnetProduct struct { - Version string `json:"version"` - Files []File `json:"files"` -} - -type File struct { - Name string `json:"name"` - Rid string `json:"rid"` - URL string `json:"url"` - Hash string `json:"hash"` -} - ////////// // Main ////////// From c8a6f5c3c17bf791ff6bed7fe3702e6abf0a6c7c Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Tue, 24 Mar 2026 16:08:04 +0000 Subject: [PATCH 5/7] ci: add dotnet to ci --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fb124d..69f56ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: "build-essential", "cypress-deps", "docker-out", + "dotnet", "eclipse-deps", "git-lfs", "gitlab-cli", From 4fd8a4871df5451e7de897371cdd811ac828e1be Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Wed, 25 Mar 2026 15:44:41 +0000 Subject: [PATCH 6/7] chore: mr feedback --- features/src/dotnet/README.md | 2 +- features/src/dotnet/devcontainer-feature.json | 5 ++-- features/src/dotnet/install.sh | 2 +- features/src/dotnet/installer.go | 27 +++++++++++-------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/features/src/dotnet/README.md b/features/src/dotnet/README.md index 5d0499a..0a8772a 100755 --- a/features/src/dotnet/README.md +++ b/features/src/dotnet/README.md @@ -23,7 +23,7 @@ A package which installs .NET SDKs, runtimes and workloads. | Option | Description | Type | Default Value | Proposals | |-----|-----|-----|-----|-----| -| version | Select or enter a .NET SDK version. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | 10.0 | latest, lts, none, 8.0, 9.0, 8.0.408 | +| version | Select or enter a .NET SDK version. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | 10.0 | lts, none, 8.0, 9.0, 10.0, 8.0.408 | | additionalVersions | Enter additional .NET SDK versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 8.0,9.0, 8.0.408 | | dotnetRuntimeVersions | Enter additional .NET runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 8.0.15, 9.0, lts, 8.0 | | aspNetCoreRuntimeVersions | Enter additional ASP.NET Core runtime versions, separated by commas. Use 'lts' for the latest LTS version, 'X.Y' or 'X.Y.Z' for a specific version. | string | <empty> | 8.0.15, lts, 8.0 | diff --git a/features/src/dotnet/devcontainer-feature.json b/features/src/dotnet/devcontainer-feature.json index 807df8d..3b54053 100644 --- a/features/src/dotnet/devcontainer-feature.json +++ b/features/src/dotnet/devcontainer-feature.json @@ -7,11 +7,11 @@ "version": { "type": "string", "proposals": [ - "latest", "lts", "none", "8.0", "9.0", + "10.0", "8.0.408" ], "default": "10.0", @@ -83,6 +83,5 @@ "DOTNET_RUNNING_IN_CONTAINER": "true", "DOTNET_USE_POLLING_FILE_WATCHER": "true", "DOTNET_CLI_TELEMETRY_OPTOUT": "1" - }, - "onCreateCommand": "dotnet nuget remove source nuget.org" + } } \ No newline at end of file diff --git a/features/src/dotnet/install.sh b/features/src/dotnet/install.sh index 61410bd..b383cc0 100755 --- a/features/src/dotnet/install.sh +++ b/features/src/dotnet/install.sh @@ -8,4 +8,4 @@ -workloads="${WORKLOADS:-""}" \ -downloadUrl="${DOWNLOADURL:-""}" \ -versionsUrl="${VERSIONSURL:-""}" \ --nugetConfigPath="${NUGETCONFIGPATH:-""}" \ +-nugetConfigPath="${NUGETCONFIGPATH:-""}" diff --git a/features/src/dotnet/installer.go b/features/src/dotnet/installer.go index 440ad13..0fbc172 100644 --- a/features/src/dotnet/installer.go +++ b/features/src/dotnet/installer.go @@ -125,12 +125,11 @@ func runMain() error { workloads: strings.Split(strings.ReplaceAll(*workloads, " ", ""), ","), }) } - // Component to create a symlink, but only if an sdk was installed - if len(allSdks) > 0 { - feature.AddComponents(&symlinkComponent{ - ComponentBase: installer.NewComponentBase("SymLink", installer.VERSION_IRRELEVANT), - }) - } + + feature.AddComponents(&symlinkComponent{ + ComponentBase: installer.NewComponentBase("SymLink", installer.VERSION_IRRELEVANT), + }) + // Process the feature return feature.Process() } @@ -270,7 +269,9 @@ func installDotnetBinary(downloadUrl string, product Product, fileName string, p arch := "" if osInfo.IsAlpine() { arch = fmt.Sprintf("linux-musl-%s", archPart) - installer.Tools.System.InstallPackages("ca-certificates", "libgcc", "libssl3", "libstdc++", "zlib", "icu-libs", "icu-data-full", "tzdata", "krb5") + if err := installer.Tools.System.InstallPackages("ca-certificates", "libgcc", "libssl3", "libstdc++", "zlib", "icu-libs", "icu-data-full", "tzdata", "krb5"); err != nil { + return err + } } else if osInfo.IsDebian() || osInfo.IsUbuntu() { arch = fmt.Sprintf("linux-%s", archPart) } else { @@ -279,7 +280,7 @@ func installDotnetBinary(downloadUrl string, product Product, fileName string, p // Download file downloadedFileName := fmt.Sprintf("%s-%s-%s.tar.gz", fileName, version.Raw, arch) - fullUrl := fmt.Sprintf("%s/%s/%s/%s", downloadUrl, product, version.Raw, downloadedFileName) + fullUrl := fmt.Sprintf("%s/%s/%s/%s", downloadUrl, product.String(), version.Raw, downloadedFileName) if err := installer.Tools.Download.ToFile(fullUrl, downloadedFileName, progressName); err != nil { return err } @@ -313,13 +314,15 @@ func resolveVersion(versionsUrl string, requestedVersion string, product Product if regexChannel.MatchString(strings.ToLower(requestedVersion)) { // 4.0 works as channel // 8.0.1xx feature band should work according to docs but does somehow work only for some versions, e.g. 8.0.2xx does not work... - latestVersionUrl := fmt.Sprintf("%s/%s/%s/latest.version", versionsUrl, product, strings.ToUpper(requestedVersion)) + latestVersionUrl := fmt.Sprintf("%s/%s/%s/latest.version", versionsUrl, product.String(), strings.ToUpper(requestedVersion)) version, err := installer.Tools.Download.AsString(latestVersionUrl) if err != nil { return "", err } - latestVersion = version + // Normalize potential CRLF line endings and trim surrounding whitespace/newlines. + normalized := strings.ReplaceAll(version, "\r\n", "\n") + latestVersion = strings.TrimSpace(normalized) } else { latestVersion = requestedVersion } @@ -359,7 +362,9 @@ func (c *nugetConfigComponent) InstallVersion(version *gover.Version) error { return err } // ensure nuget.org source is disabled and we only access sources defined in the provided config file - execr.Run(true, "dotnet", "nuget", "disable", "source", "nuget.org") + if err := execr.Run(true, "dotnet", "nuget", "disable", "source", "nuget.org"); err != nil { + return fmt.Errorf("failed to disable nuget.org NuGet source: %w", err) + } if err := os.MkdirAll("/etc/opt/NuGet", 0755); err != nil { return err } From f1e542f94656022fddc4c4359811019a87f7cd2d Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Mon, 30 Mar 2026 07:06:35 +0000 Subject: [PATCH 7/7] chore: mr feedback --- features/src/dotnet/NOTES.md | 4 ++-- features/src/dotnet/README.md | 4 ++-- features/src/dotnet/install.sh | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/features/src/dotnet/NOTES.md b/features/src/dotnet/NOTES.md index e627ae8..882322e 100644 --- a/features/src/dotnet/NOTES.md +++ b/features/src/dotnet/NOTES.md @@ -1,7 +1,7 @@ ## Dotnet Tools -If you need additional tools for example like the Powerapps CLI you can install them using `dotnet tool install --create-manifest-if-needed `. -This installs the tool creates a manifest file: `.config/dotnet-tools.json`. +If you need additional tools, such as the PowerApps CLI, you can install them by running `dotnet tool install --create-manifest-if-needed `. +This installs the tool and creates a manifest file: `.config/dotnet-tools.json`. ```json { diff --git a/features/src/dotnet/README.md b/features/src/dotnet/README.md index 0a8772a..585dffe 100755 --- a/features/src/dotnet/README.md +++ b/features/src/dotnet/README.md @@ -40,8 +40,8 @@ A package which installs .NET SDKs, runtimes and workloads. ## Dotnet Tools -If you need additional tools for example like the Powerapps CLI you can install them using `dotnet tool install --create-manifest-if-needed `. -This installs the tool creates a manifest file: `.config/dotnet-tools.json`. +If you need additional tools, such as the PowerApps CLI, you can install them by running `dotnet tool install --create-manifest-if-needed `. +This installs the tool and creates a manifest file: `.config/dotnet-tools.json`. ```json { diff --git a/features/src/dotnet/install.sh b/features/src/dotnet/install.sh index b383cc0..79273ee 100755 --- a/features/src/dotnet/install.sh +++ b/features/src/dotnet/install.sh @@ -1,11 +1,11 @@ . ./functions.sh "./installer_$(detect_arch)" \ --version="${VERSION:-"latest"}" \ --additionalVersions="${ADDITIONALVERSIONS:-""}" \ --dotnetRuntimeVersions="${DOTNETRUNTIMEVERSIONS:-""}" \ --aspNetCoreRuntimeVersions="${ASPNETCORERUNTIMEVERSIONS:-""}" \ --workloads="${WORKLOADS:-""}" \ --downloadUrl="${DOWNLOADURL:-""}" \ --versionsUrl="${VERSIONSURL:-""}" \ --nugetConfigPath="${NUGETCONFIGPATH:-""}" + -version="${VERSION:-"latest"}" \ + -additionalVersions="${ADDITIONALVERSIONS:-""}" \ + -dotnetRuntimeVersions="${DOTNETRUNTIMEVERSIONS:-""}" \ + -aspNetCoreRuntimeVersions="${ASPNETCORERUNTIMEVERSIONS:-""}" \ + -workloads="${WORKLOADS:-""}" \ + -downloadUrl="${DOWNLOADURL:-""}" \ + -versionsUrl="${VERSIONSURL:-""}" \ + -nugetConfigPath="${NUGETCONFIGPATH:-""}"