diff --git a/platforms/__common.sh b/platforms/__common.sh
index 60b501ab53..a63c95e3bd 100644
--- a/platforms/__common.sh
+++ b/platforms/__common.sh
@@ -148,6 +148,50 @@ getSdkFromImage() {
buildPlatform() {
local versionFile="$1"
local funcToCall="$2"
+
+ # When VERSIONS_TO_BUILD_OVERRIDE is set (comma-separated list of versions),
+ # only build those specific versions and skip blob existence checks entirely.
+ # This allows force-building specific SDK versions without rebuilding everything.
+ if [ -n "$VERSIONS_TO_BUILD_OVERRIDE" ]; then
+ echo "VERSIONS_TO_BUILD_OVERRIDE is set: $VERSIONS_TO_BUILD_OVERRIDE"
+ echo "Building only specified versions, skipping storage account checks."
+ export OVERWRITE_EXISTING_SDKS="true"
+
+ # Build a lookup set of requested versions
+ IFS=',' read -ra _requested_versions <<< "$VERSIONS_TO_BUILD_OVERRIDE"
+ declare -A _force_set
+ for _v in "${_requested_versions[@]}"; do
+ _v="$(echo "$_v" | xargs)"
+ [ -n "$_v" ] && _force_set["$_v"]=1
+ done
+
+ # Read the version file but only invoke the build function for matching versions.
+ # This preserves extra args (e.g. GPG keys, SHAs) that some platforms need.
+ while IFS= read -r VERSION_INFO || [[ -n $VERSION_INFO ]]; do
+ VERSION_INFO="$(echo -e "${VERSION_INFO}" | sed -e 's/^[[:space:]]*//')"
+ if [ -z "$VERSION_INFO" ] || [[ $VERSION_INFO = \#* ]]; then
+ continue
+ fi
+
+ IFS=',' read -ra VERSION_INFO_PARTS <<< "$VERSION_INFO"
+ lineVersion="$(echo -e "${VERSION_INFO_PARTS[0]}" | sed -e 's/^[[:space:]]*//')"
+
+ if [ -z "${_force_set[$lineVersion]:-}" ]; then
+ continue
+ fi
+
+ echo "Force-building version: $lineVersion"
+ versionArgs=()
+ for arg in "${VERSION_INFO_PARTS[@]}"; do
+ arg="$(echo -e "${arg}" | sed -e 's/^[[:space:]]*//')"
+ versionArgs+=("$arg")
+ done
+
+ $funcToCall "${versionArgs[@]}"
+ done < "$versionFile"
+ return
+ fi
+
while IFS= read -r VERSION_INFO || [[ -n $VERSION_INFO ]]
do
# remove all whitespace before first character
diff --git a/platforms/nodejs/Dockerfile b/platforms/nodejs/Dockerfile
index 8e860f31a0..60a02e170f 100644
--- a/platforms/nodejs/Dockerfile
+++ b/platforms/nodejs/Dockerfile
@@ -2,6 +2,8 @@ ARG OS_FLAVOR
FROM mcr.microsoft.com/mirror/docker/library/buildpack-deps:${OS_FLAVOR}
ARG OS_FLAVOR
ENV OS_FLAVOR=$OS_FLAVOR
+ARG VERSIONS_TO_BUILD_OVERRIDE=""
+ENV VERSIONS_TO_BUILD_OVERRIDE=$VERSIONS_TO_BUILD_OVERRIDE
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
diff --git a/platforms/php/Dockerfile b/platforms/php/Dockerfile
index 82f3df6d86..bc400a3a43 100644
--- a/platforms/php/Dockerfile
+++ b/platforms/php/Dockerfile
@@ -2,6 +2,8 @@ ARG OS_FLAVOR
FROM mcr.microsoft.com/mirror/docker/library/buildpack-deps:${OS_FLAVOR} AS php-buildpack-prereqs
ARG OS_FLAVOR
ENV OS_FLAVOR=$OS_FLAVOR
+ARG VERSIONS_TO_BUILD_OVERRIDE=""
+ENV VERSIONS_TO_BUILD_OVERRIDE=$VERSIONS_TO_BUILD_OVERRIDE
COPY platforms/php/prereqs /php
COPY platforms/php/prereqs/build.sh /tmp/
COPY images/receiveGpgKeys.sh /tmp/receiveGpgKeys.sh
diff --git a/platforms/php/composer/Dockerfile b/platforms/php/composer/Dockerfile
index a747039039..03d51cdd8e 100644
--- a/platforms/php/composer/Dockerfile
+++ b/platforms/php/composer/Dockerfile
@@ -2,6 +2,8 @@ ARG OS_FLAVOR
FROM mcr.microsoft.com/mirror/docker/library/buildpack-deps:${OS_FLAVOR} AS php-buildpack-prereqs
ARG OS_FLAVOR
ENV OS_FLAVOR=$OS_FLAVOR
+ARG VERSIONS_TO_BUILD_OVERRIDE=""
+ENV VERSIONS_TO_BUILD_OVERRIDE=$VERSIONS_TO_BUILD_OVERRIDE
COPY platforms/php/prereqs /php
# COPY build/__phpVersions.sh /php/
COPY platforms/php/prereqs/build.sh /tmp/
diff --git a/platforms/python/Dockerfile b/platforms/python/Dockerfile
index 0a837c0cb1..ef5603f788 100644
--- a/platforms/python/Dockerfile
+++ b/platforms/python/Dockerfile
@@ -3,6 +3,8 @@ ARG OS_FLAVOR
FROM mcr.microsoft.com/mirror/docker/library/buildpack-deps:${OS_FLAVOR}
ARG OS_FLAVOR
ENV OS_FLAVOR=$OS_FLAVOR
+ARG VERSIONS_TO_BUILD_OVERRIDE=""
+ENV VERSIONS_TO_BUILD_OVERRIDE=$VERSIONS_TO_BUILD_OVERRIDE
# COPY build/__pythonVersions.sh /tmp/
diff --git a/src/BuildScriptGenerator.Common/SdkImageRepositoryHelper.cs b/src/BuildScriptGenerator.Common/SdkImageRepositoryHelper.cs
new file mode 100644
index 0000000000..40a8832b66
--- /dev/null
+++ b/src/BuildScriptGenerator.Common/SdkImageRepositoryHelper.cs
@@ -0,0 +1,21 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Common
+{
+ public static class SdkImageRepositoryHelper
+ {
+ ///
+ /// Maps a platform name to its OCI SDK image repository path.
+ /// e.g. "nodejs" → "oryx/nodejs-sdk", "php" → "oryx/php-sdk".
+ /// Final image ref: mcr.microsoft.com/oryx/nodejs-sdk:bookworm-20.20.2
+ ///
+ public static string GetSdkImageRepository(string platformName, string prefix = null)
+ {
+ prefix = string.IsNullOrEmpty(prefix) ? SdkStorageConstants.DefaultAcrSdkRepositoryPrefix : prefix;
+ return $"{prefix}/{platformName}-sdk";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator.Common/SdkStorageConstants.cs b/src/BuildScriptGenerator.Common/SdkStorageConstants.cs
index 92c1ba0920..59858b9507 100644
--- a/src/BuildScriptGenerator.Common/SdkStorageConstants.cs
+++ b/src/BuildScriptGenerator.Common/SdkStorageConstants.cs
@@ -21,5 +21,9 @@ public static class SdkStorageConstants
public const string DotnetRuntimeVersionMetadataName = "Dotnet_runtime_version";
public const string LegacyDotnetRuntimeVersionMetadataName = "Runtime_version";
public const string OsTypeMetadataName = "Os_type";
+
+ // OCI image based SDK distribution constants
+ public const string DefaultAcrSdkRegistryUrl = "https://mcr.microsoft.com";
+ public const string DefaultAcrSdkRepositoryPrefix = "oryx";
}
}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/AcrSdkProvider.cs b/src/BuildScriptGenerator/AcrSdkProvider.cs
new file mode 100644
index 0000000000..ab81a0ef83
--- /dev/null
+++ b/src/BuildScriptGenerator/AcrSdkProvider.cs
@@ -0,0 +1,212 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Formats.Tar;
+using System.IO;
+using System.IO.Compression;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator.Common;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ ///
+ /// Fetches SDK tarballs directly from an OCI container registry.
+ /// SDK images are single-layer FROM scratch images containing a single
+ /// .tar.gz SDK file. The OCI layer blob is a tar archive of the image
+ /// filesystem, so this provider downloads the layer, extracts the inner SDK
+ /// tarball from it, and caches it locally.
+ ///
+ ///
+ /// Makes direct HTTP calls to the registry (no Unix socket).
+ /// See for the socket-based variant.
+ ///
+ public class AcrSdkProvider : IAcrSdkProvider
+ {
+ private readonly ILogger logger;
+ private readonly IStandardOutputWriter outputWriter;
+ private readonly BuildScriptGeneratorOptions options;
+ private readonly OciRegistryClient ociClient;
+
+ public AcrSdkProvider(
+ IStandardOutputWriter outputWriter,
+ ILogger logger,
+ IOptions options,
+ OciRegistryClient ociClient)
+ {
+ this.logger = logger;
+ this.outputWriter = outputWriter;
+ this.options = options.Value;
+ this.ociClient = ociClient;
+ }
+
+ ///
+ public async Task RequestSdkFromAcrAsync(string platformName, string version, string debianFlavor, string runtimeVersion = null)
+ {
+ if (string.IsNullOrEmpty(platformName))
+ {
+ throw new ArgumentException("Platform name cannot be null or empty.", nameof(platformName));
+ }
+
+ if (string.IsNullOrEmpty(version))
+ {
+ throw new ArgumentException("Version cannot be null or empty.", nameof(version));
+ }
+
+ if (string.IsNullOrEmpty(debianFlavor))
+ {
+ debianFlavor = this.options.DebianFlavor ?? "bookworm";
+ }
+
+ var repository = SdkImageRepositoryHelper.GetSdkImageRepository(platformName, this.options.OryxAcrSdkRepositoryPrefix);
+ var tag = string.IsNullOrEmpty(runtimeVersion)
+ ? $"{debianFlavor}-{version}"
+ : $"{debianFlavor}-{version}_{runtimeVersion}";
+ var blobName = $"{platformName}-{debianFlavor}-{version}.tar.gz";
+
+ this.logger.LogInformation(
+ "Requesting SDK from ACR: {Repository}:{Tag}",
+ repository,
+ tag);
+ this.outputWriter.WriteLine(
+ $"Requesting SDK from ACR: {repository}:{tag}");
+
+ // Download to the writable dynamic install directory, NOT /var/OryxSdks (read-only external mount).
+ var downloadDir = Path.Combine(this.options.DynamicInstallRootDir, platformName);
+ var tarballPath = Path.Combine(downloadDir, blobName);
+ var digestPath = Path.Combine(downloadDir, $".{blobName}.digest");
+
+ try
+ {
+ // Get image manifest
+ var remoteDigest = await this.ociClient.GetManifestDigestAsync(repository, tag);
+
+ // Check if cached tarball is still fresh
+ if (File.Exists(tarballPath) && File.Exists(digestPath) && remoteDigest != null)
+ {
+ var localDigest = File.ReadAllText(digestPath).Trim();
+ if (string.Equals(localDigest, remoteDigest, StringComparison.OrdinalIgnoreCase))
+ {
+ this.logger.LogInformation(
+ "SDK cache is fresh (digest match): {FilePath}",
+ tarballPath);
+ this.outputWriter.WriteLine(
+ $"SDK tarball already cached and fresh at {tarballPath}");
+ return true;
+ }
+
+ this.logger.LogInformation(
+ "SDK cache is stale (digest mismatch). Re-downloading.");
+ }
+
+ // Get manifest → extract single layer digest
+ var manifest = await this.ociClient.GetManifestAsync(repository, tag);
+ var layerDigest = OciRegistryClient.GetFirstLayerDigest(manifest);
+
+ if (string.IsNullOrEmpty(layerDigest))
+ {
+ this.logger.LogWarning(
+ "No layer found in manifest for {Repository}:{Tag}",
+ repository,
+ tag);
+ this.outputWriter.WriteLine($"No layer found in ACR manifest for {platformName} {version}.");
+ return false;
+ }
+
+ Directory.CreateDirectory(downloadDir);
+
+ // 2. Download the OCI layer blob to a temp file.
+ // The layer is a tar archive of the image filesystem (not the SDK tarball itself).
+ var layerTempPath = Path.Combine(downloadDir, $".layer-{Guid.NewGuid():N}.tmp");
+ try
+ {
+ var downloadSuccess = await this.ociClient.DownloadLayerBlobAsync(
+ repository,
+ layerDigest,
+ layerTempPath);
+
+ if (!downloadSuccess)
+ {
+ this.logger.LogWarning(
+ "ACR SDK pull failed digest verification: {Repository}:{Tag}",
+ repository,
+ tag);
+ this.outputWriter.WriteLine(
+ $"Failed to pull SDK from ACR (digest mismatch): {platformName} {version}");
+ return false;
+ }
+
+ // 3. Extract the inner SDK .tar.gz from the layer tar.
+ // The image is FROM scratch with a single COPY of the SDK tarball,
+ // so the layer contains the .tar.gz as a top-level entry.
+ this.ExtractFileFromTar(layerTempPath, tarballPath, blobName);
+ }
+ finally
+ {
+ // Always clean up the temporary layer file
+ if (File.Exists(layerTempPath))
+ {
+ File.Delete(layerTempPath);
+ }
+ }
+
+ this.logger.LogInformation(
+ "Successfully pulled SDK from ACR: {Repository}:{Tag} → {FilePath}",
+ repository,
+ tag,
+ tarballPath);
+ this.outputWriter.WriteLine(
+ $"Successfully pulled SDK from ACR: {platformName} {version}");
+
+ // Write manifest digest sidecar for future freshness checks
+ if (!string.IsNullOrEmpty(remoteDigest))
+ {
+ File.WriteAllText(digestPath, remoteDigest);
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(
+ ex,
+ "Error pulling SDK from ACR: {Repository}:{Tag}",
+ repository,
+ tag);
+ this.outputWriter.WriteLine(
+ $"Error pulling SDK from ACR: {platformName} {version}: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Extracts the expected SDK .tar.gz file from an OCI layer tar archive.
+ /// OCI layers use media type "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ /// so the blob must be decompressed before reading tar entries.
+ ///
+ private void ExtractFileFromTar(string layerPath, string outputPath, string expectedFileName)
+ {
+ using (var stream = File.OpenRead(layerPath))
+ using (var gzipStream = new GZipStream(stream, CompressionMode.Decompress))
+ using (var tarReader = new TarReader(gzipStream))
+ {
+ TarEntry entry;
+ while ((entry = tarReader.GetNextEntry()) != null)
+ {
+ var name = entry.Name.TrimStart('.', '/');
+ if (entry.DataStream != null && name.Equals(expectedFileName, StringComparison.OrdinalIgnoreCase))
+ {
+ entry.ExtractToFile(outputPath, overwrite: true);
+ return;
+ }
+ }
+ }
+
+ throw new InvalidOperationException($"Expected entry '{expectedFileName}' not found in OCI layer: {layerPath}");
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/AcrVersionProviderBase.cs b/src/BuildScriptGenerator/AcrVersionProviderBase.cs
new file mode 100644
index 0000000000..242a3b184b
--- /dev/null
+++ b/src/BuildScriptGenerator/AcrVersionProviderBase.cs
@@ -0,0 +1,95 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator.Common;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ ///
+ /// Base class for ACR-based SDK version providers. Parallel to
+ /// but discovers versions via OCI Distribution API (tag listing) instead of
+ /// Azure Blob Storage listing with XML metadata.
+ /// Default versions come from local per-flavor constants rather than ACR image labels.
+ ///
+ public class AcrVersionProviderBase
+ {
+ private readonly ILogger logger;
+ private readonly string debianFlavor;
+ private readonly string repositoryPrefix;
+
+ public AcrVersionProviderBase(
+ IOptions commonOptions,
+ OciRegistryClient ociClient,
+ ILoggerFactory loggerFactory)
+ {
+ var options = commonOptions.Value;
+ this.logger = loggerFactory.CreateLogger(this.GetType());
+ this.debianFlavor = options.DebianFlavor;
+ this.repositoryPrefix = options.OryxAcrSdkRepositoryPrefix;
+ this.OciClient = ociClient;
+ }
+
+ protected OciRegistryClient OciClient { get; }
+
+ ///
+ /// Lists available versions for a platform from ACR tags and resolves the default
+ /// version from the supplied per-flavor dictionary.
+ ///
+ protected PlatformVersionInfo GetAvailableVersionsFromAcr(
+ string platformName,
+ Dictionary defaultVersionPerFlavor)
+ {
+ var repository = SdkImageRepositoryHelper.GetSdkImageRepository(platformName, this.repositoryPrefix);
+
+ this.logger.LogDebug("Getting available versions for {platformName} from ACR repository {repository}.", platformName, repository);
+
+ var allTags = this.GetTags(repository);
+ var supportedVersions = this.FilterVersionTags(allTags);
+
+ string defaultVersion = null;
+ if (defaultVersionPerFlavor != null &&
+ !string.IsNullOrEmpty(this.debianFlavor) &&
+ defaultVersionPerFlavor.TryGetValue(this.debianFlavor, out var version))
+ {
+ defaultVersion = version;
+ }
+
+ this.logger.LogDebug(
+ "Found {count} versions for {platformName} on ACR (default: {default}).",
+ supportedVersions.Count,
+ platformName,
+ defaultVersion ?? "none");
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(supportedVersions, defaultVersion);
+ }
+
+ private List GetTags(string repository)
+ {
+ try
+ {
+ return this.OciClient.GetAllTagsAsync(repository).GetAwaiter().GetResult();
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Failed to get tags from ACR for {repository}.", repository);
+ throw;
+ }
+ }
+
+ private List FilterVersionTags(List allTags)
+ {
+ var prefix = $"{this.debianFlavor}-";
+ return allTags
+ .Where(t => t.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ .Select(t => t.Substring(prefix.Length))
+ .ToList();
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/BuildScriptGenerator.csproj b/src/BuildScriptGenerator/BuildScriptGenerator.csproj
index fe68cd3216..9151e7a866 100644
--- a/src/BuildScriptGenerator/BuildScriptGenerator.csproj
+++ b/src/BuildScriptGenerator/BuildScriptGenerator.csproj
@@ -22,11 +22,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/src/BuildScriptGenerator/BuildScriptGeneratorServiceCollectionExtensions.cs b/src/BuildScriptGenerator/BuildScriptGeneratorServiceCollectionExtensions.cs
index dc63ad3c89..fb966a870b 100644
--- a/src/BuildScriptGenerator/BuildScriptGeneratorServiceCollectionExtensions.cs
+++ b/src/BuildScriptGenerator/BuildScriptGeneratorServiceCollectionExtensions.cs
@@ -9,6 +9,9 @@
using System.Net.Http.Headers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator.Common;
using Microsoft.Oryx.Detector;
using Polly;
using Polly.Extensions.Http;
@@ -41,11 +44,33 @@ public static IServiceCollection AddBuildScriptGeneratorServices(this IServiceCo
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(sp =>
+ {
+ var opts = sp.GetRequiredService>().Value;
+ var registryUrl = string.IsNullOrEmpty(opts.OryxAcrSdkRegistryUrl)
+ ? SdkStorageConstants.DefaultAcrSdkRegistryUrl
+ : opts.OryxAcrSdkRegistryUrl;
+ return new OciRegistryClient(
+ registryUrl,
+ sp.GetRequiredService(),
+ sp.GetRequiredService());
+ });
services.AddHttpClient("general", httpClient =>
{
// NOTE: Setting user agent is required to avoid receiving 403 Forbidden response.
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("oryx", "1.0"));
- }).AddPolicyHandler(GetRetryPolicy());
+ })
+ .RedactLoggedHeaders(header => header.Equals("Authorization", StringComparison.OrdinalIgnoreCase))
+ .AddPolicyHandler(GetRetryPolicy());
+
+ services.AddHttpClient("acr", httpClient =>
+ {
+ httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("oryx", "1.0"));
+ })
+ .RedactLoggedHeaders(header => header.Equals("Authorization", StringComparison.OrdinalIgnoreCase))
+ .AddPolicyHandler(GetAcrRetryPolicy());
// Add all checkers (platform-dependent + platform-independent)
foreach (Type type in typeof(BuildScriptGeneratorServiceCollectionExtensions).Assembly.GetTypes())
@@ -68,5 +93,16 @@ private static IAsyncPolicy GetRetryPolicy()
retryCount: 6,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
+
+ private static IAsyncPolicy GetAcrRetryPolicy()
+ {
+ // ACR-specific: only retry transient errors, NOT 404s.
+ // Missing OCI tags are deterministic
+ return HttpPolicyExtensions
+ .HandleTransientHttpError()
+ .WaitAndRetryAsync(
+ retryCount: 3,
+ retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
+ }
}
}
diff --git a/src/BuildScriptGenerator/Contracts/IAcrSdkProvider.cs b/src/BuildScriptGenerator/Contracts/IAcrSdkProvider.cs
new file mode 100644
index 0000000000..e0eeee91b1
--- /dev/null
+++ b/src/BuildScriptGenerator/Contracts/IAcrSdkProvider.cs
@@ -0,0 +1,36 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System.Threading.Tasks;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ ///
+ /// Fetches SDK tarballs directly from an OCI container registry.
+ /// SDK images are single-layer FROM scratch images where
+ /// the layer IS the SDK tarball.
+ ///
+ ///
+ /// Gated by the ORYX_ENABLE_ACR_SDK_PROVIDER feature flag.
+ /// Alternative to (blob storage via socket).
+ ///
+ public interface IAcrSdkProvider
+ {
+ ///
+ /// Pulls an SDK image from the registry and saves the tarball to
+ /// the dynamic install directory (writable by Oryx).
+ ///
+ /// The platform name (e.g., "nodejs", "python", "dotnet", "php").
+ /// The SDK version (e.g., "20.19.3").
+ /// The Debian flavor (e.g., "bookworm", "bullseye").
+ ///
+ /// Optional runtime version for platforms whose ACR tags encode both SDK and runtime
+ /// versions (e.g., .NET tags use "{osFlavor}-{sdkVersion}_{runtimeVersion}").
+ /// When provided, the tag is constructed as "{debianFlavor}-{version}_{runtimeVersion}".
+ ///
+ /// True if the SDK tarball was successfully downloaded.
+ Task RequestSdkFromAcrAsync(string platformName, string version, string debianFlavor, string runtimeVersion = null);
+ }
+}
diff --git a/src/BuildScriptGenerator/Contracts/IExternalAcrSdkProvider.cs b/src/BuildScriptGenerator/Contracts/IExternalAcrSdkProvider.cs
new file mode 100644
index 0000000000..a67613e75c
--- /dev/null
+++ b/src/BuildScriptGenerator/Contracts/IExternalAcrSdkProvider.cs
@@ -0,0 +1,30 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System.Threading.Tasks;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ ///
+ /// Pulls SDK tarballs from ACR via an external host over a Unix socket.
+ /// This is the ACR equivalent of (blob storage via socket).
+ ///
+ ///
+ /// Flow: Oryx → Unix socket → external host → ACR.
+ /// Gated by the ORYX_ENABLE_EXTERNAL_ACR_SDK_PROVIDER feature flag.
+ /// Version discovery is handled separately by .
+ ///
+ public interface IExternalAcrSdkProvider
+ {
+ ///
+ /// Pulls an SDK tarball from ACR via the external provider and places it in the local cache.
+ ///
+ /// The platform name (e.g., "python", "nodejs", "dotnet", "php").
+ /// The SDK version.
+ /// The Debian flavor (e.g., "bookworm", "bullseye").
+ /// True if the SDK was successfully pulled and cached.
+ Task RequestSdkAsync(string platformName, string version, string debianFlavor);
+ }
+}
diff --git a/src/BuildScriptGenerator/Contracts/IExternalSdkProvider.cs b/src/BuildScriptGenerator/Contracts/IExternalSdkProvider.cs
index 143be2b025..e8d62a7513 100644
--- a/src/BuildScriptGenerator/Contracts/IExternalSdkProvider.cs
+++ b/src/BuildScriptGenerator/Contracts/IExternalSdkProvider.cs
@@ -17,7 +17,7 @@ public interface IExternalSdkProvider
///
/// The directory where SDKs are cached by the external provider.
///
- public const string ExternalSdksStorageDir = "/var/OryxSdksCache";
+ public const string ExternalSdksStorageDir = "/var/OryxSdks";
///
/// Gets all metadata for a specific platform from the external SDK provider.
diff --git a/src/BuildScriptGenerator/DefaultPlatformsInformationProvider.cs b/src/BuildScriptGenerator/DefaultPlatformsInformationProvider.cs
index 8c11d5634e..e722453e47 100644
--- a/src/BuildScriptGenerator/DefaultPlatformsInformationProvider.cs
+++ b/src/BuildScriptGenerator/DefaultPlatformsInformationProvider.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.Oryx.BuildScriptGenerator
@@ -16,15 +17,18 @@ public class DefaultPlatformsInformationProvider
{
private readonly IEnumerable platforms;
private readonly IStandardOutputWriter outputWriter;
+ private readonly ILogger logger;
private readonly BuildScriptGeneratorOptions commonOptions;
public DefaultPlatformsInformationProvider(
IEnumerable platforms,
IStandardOutputWriter outputWriter,
+ ILogger logger,
IOptions commonOptions)
{
this.platforms = platforms;
this.outputWriter = outputWriter;
+ this.logger = logger;
this.commonOptions = commonOptions.Value;
}
@@ -39,6 +43,19 @@ public IEnumerable GetPlatformsInfo(RepositoryContext context)
this.outputWriter.WriteLine($"Primary SDK Storage URL: {this.commonOptions.OryxSdkStorageBaseUrl}");
this.outputWriter.WriteLine($"Backup SDK Storage URL: {this.commonOptions.OryxSdkStorageBackupBaseUrl}");
+ this.outputWriter.WriteLine($"ACR SDK Registry URL: {this.commonOptions.OryxAcrSdkRegistryUrl ?? "(not set)"}");
+
+ // Log SDK provider status and resolution priority
+ this.outputWriter.WriteLine("SDK provider status:");
+ this.outputWriter.WriteLine($" External ACR SDK provider: {(this.commonOptions.EnableExternalAcrSdkProvider ? "Enabled" : "Disabled")}");
+ this.outputWriter.WriteLine($" External SDK provider: {(this.commonOptions.EnableExternalSdkProvider ? "Enabled" : "Disabled")}");
+ this.outputWriter.WriteLine($" Direct ACR SDK provider: {(this.commonOptions.EnableAcrSdkProvider ? "Enabled" : "Disabled")}");
+ this.outputWriter.WriteLine($" Blob SDK provider: Enabled");
+
+ if (this.commonOptions.EnableExternalAcrSdkProvider && !string.IsNullOrEmpty(this.commonOptions.PlatformName))
+ {
+ this.outputWriter.WriteLine($"External ACR SDK provider is enabled. Only using user-specified platform: {this.commonOptions.PlatformName}");
+ }
// Try detecting ALL platforms since in some scenarios this is required.
// For example, in case of a multi-platform app like ASP.NET Core + NodeJs, we might need to dynamically
@@ -47,18 +64,10 @@ public IEnumerable GetPlatformsInfo(RepositoryContext context)
// build environment is setup with detected platforms' sdks.
this.outputWriter.WriteLine("Detecting platforms...");
- if (this.commonOptions.EnableExternalSdkProvider)
- {
- this.outputWriter.WriteLine("External SDK provider is enabled.");
- }
-
foreach (var platform in this.platforms)
{
- // Check if a platform is enabled or not
- if (!platform.IsEnabled(context))
+ if (!this.ShouldDetectPlatform(platform, context))
{
- this.outputWriter.WriteLine(
- $"Platform '{platform.Name}' has been disabled, so skipping detection for it.");
continue;
}
@@ -92,5 +101,27 @@ public IEnumerable GetPlatformsInfo(RepositoryContext context)
return platformInfos;
}
+
+ private bool ShouldDetectPlatform(IProgrammingPlatform platform, RepositoryContext context)
+ {
+ // Check if a platform is enabled or not
+ if (!platform.IsEnabled(context))
+ {
+ this.outputWriter.WriteLine(
+ $"Platform '{platform.Name}' has been disabled, so skipping detection for it.");
+ return false;
+ }
+
+ if (this.commonOptions.EnableExternalAcrSdkProvider && !string.IsNullOrEmpty(this.commonOptions.PlatformName) && platform.Name != this.commonOptions.PlatformName)
+ {
+ this.logger.LogDebug(
+ "Skipping detection for platform '{PlatformName}' because External ACR SDK provider is enabled and only user provided platform '{UserPlatform}' is considered.",
+ platform.Name,
+ this.commonOptions.PlatformName);
+ return false;
+ }
+
+ return true;
+ }
}
}
diff --git a/src/BuildScriptGenerator/DotNetCore/DotnetCoreConstants.cs b/src/BuildScriptGenerator/DotNetCore/DotnetCoreConstants.cs
index 6b00b4bdd9..91f640b627 100644
--- a/src/BuildScriptGenerator/DotNetCore/DotnetCoreConstants.cs
+++ b/src/BuildScriptGenerator/DotNetCore/DotnetCoreConstants.cs
@@ -3,6 +3,8 @@
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
+using System.Collections.Generic;
+
namespace Microsoft.Oryx.BuildScriptGenerator.DotNetCore
{
public static class DotNetCoreConstants
@@ -57,5 +59,18 @@ public static class DotNetCoreConstants
public const string InstallBlazorWebAssemblyAOTWorkloadCommand = "dotnet workload install wasm-tools";
public static readonly string DefaultDotNetCoreSdkVersionsInstallDir = $"/opt/{PlatformName}";
+
+ ///
+ /// Default .NET major.minor runtime version per OS flavor, matching platforms/dotnet/versions/*/defaultVersion.txt.
+ ///
+ public static readonly Dictionary DefaultVersionPerFlavor = new Dictionary
+ {
+ { "bookworm", "8.0" },
+ { "bullseye", "6.0" },
+ { "buster", "6.0" },
+ { "focal-scm", "6.0" },
+ { "noble", "10.0" },
+ { "stretch", "6.0" },
+ };
}
}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/DotNetCore/DotnetCorePlatform.cs b/src/BuildScriptGenerator/DotNetCore/DotnetCorePlatform.cs
index afeb21c9f3..1d84617159 100644
--- a/src/BuildScriptGenerator/DotNetCore/DotnetCorePlatform.cs
+++ b/src/BuildScriptGenerator/DotNetCore/DotnetCorePlatform.cs
@@ -28,6 +28,7 @@ namespace Microsoft.Oryx.BuildScriptGenerator.DotNetCore
internal class DotNetCorePlatform : IProgrammingPlatform
{
private readonly IDotNetCoreVersionProvider versionProvider;
+ private readonly DotNetCoreExternalAcrVersionProvider externalAcrVersionProvider;
private readonly ILogger logger;
private readonly IDotNetCorePlatformDetector detector;
private readonly DotNetCoreScriptGeneratorOptions dotNetCoreScriptGeneratorOptions;
@@ -35,7 +36,10 @@ internal class DotNetCorePlatform : IProgrammingPlatform
private readonly DotNetCorePlatformInstaller platformInstaller;
private readonly GlobalJsonSdkResolver globalJsonSdkResolver;
private readonly IExternalSdkProvider externalSdkProvider;
+ private readonly IExternalAcrSdkProvider externalAcrSdkProvider;
+ private readonly IAcrSdkProvider acrSdkProvider;
private readonly TelemetryClient telemetryClient;
+ private readonly IStandardOutputWriter outputWriter;
///
/// Initializes a new instance of the class.
@@ -49,6 +53,7 @@ internal class DotNetCorePlatform : IProgrammingPlatform
/// The .
public DotNetCorePlatform(
IDotNetCoreVersionProvider versionProvider,
+ DotNetCoreExternalAcrVersionProvider externalAcrVersionProvider,
ILogger logger,
IDotNetCorePlatformDetector detector,
IOptions commonOptions,
@@ -56,9 +61,13 @@ public DotNetCorePlatform(
DotNetCorePlatformInstaller platformInstaller,
GlobalJsonSdkResolver globalJsonSdkResolver,
IExternalSdkProvider externalSdkProvider,
- TelemetryClient telemetryClient)
+ IExternalAcrSdkProvider externalAcrSdkProvider,
+ IAcrSdkProvider acrSdkProvider,
+ TelemetryClient telemetryClient,
+ IStandardOutputWriter outputWriter)
{
this.versionProvider = versionProvider;
+ this.externalAcrVersionProvider = externalAcrVersionProvider;
this.logger = logger;
this.detector = detector;
this.dotNetCoreScriptGeneratorOptions = dotNetCoreScriptGeneratorOptions.Value;
@@ -66,7 +75,10 @@ public DotNetCorePlatform(
this.platformInstaller = platformInstaller;
this.globalJsonSdkResolver = globalJsonSdkResolver;
this.externalSdkProvider = externalSdkProvider;
+ this.externalAcrSdkProvider = externalAcrSdkProvider;
+ this.acrSdkProvider = acrSdkProvider;
this.telemetryClient = telemetryClient;
+ this.outputWriter = outputWriter;
}
///
@@ -147,7 +159,7 @@ public BuildScriptSnippet GenerateBashBuildScriptSnippet(
installBlazorWebAssemblyAOTWorkloadCommand = DotNetCoreConstants.InstallBlazorWebAssemblyAOTWorkloadCommand;
manifestFileProperties[ManifestFilePropertyKeys.Frameworks] = "blazor";
this.logger.LogInformation("Detected the following frameworks: blazor");
- Console.WriteLine("Detected the following frameworks: blazor");
+ this.outputWriter.WriteLine("Detected the following frameworks: blazor");
}
var templateProperties = new DotNetCoreBashBuildSnippetProperties
@@ -233,55 +245,62 @@ public string GetInstallerScriptSnippet(
$"'{typeof(DotNetCorePlatformDetectorResult)}' but got '{detectorResult.GetType()}'.");
}
- string installationScriptSnippet = null;
- if (this.commonOptions.EnableDynamicInstall)
+ if (!this.commonOptions.EnableDynamicInstall)
{
- this.logger.LogDebug("Dynamic install is enabled.");
+ this.logger.LogDebug("Dynamic install is not enabled.");
+ return null;
+ }
+
+ this.logger.LogDebug("Dynamic install is enabled.");
+
+ var sdkVersion = dotNetCorePlatformDetectorResult.SdkVersion;
+
+ if (this.platformInstaller.IsVersionAlreadyInstalled(sdkVersion))
+ {
+ this.logger.LogDebug(
+ "DotNetCore SDK version {globalJsonSdkVersion} is already installed. So skipping installing it again.",
+ sdkVersion);
+ return null;
+ }
- if (this.platformInstaller.IsVersionAlreadyInstalled(dotNetCorePlatformDetectorResult.SdkVersion))
+ // Priority: External SDK ACR → External-SDK → Direct-ACR → CDN
+ // 1. Try External SDK ACR (socket → ACR)
+ if (this.commonOptions.EnableExternalAcrSdkProvider)
+ {
+ var result = this.TryInstallFromExternalAcrSdkProvider(sdkVersion);
+ if (result != null)
{
- this.logger.LogDebug("DotNetCore SDK version {globalJsonSdkVersion} is already installed. So skipping installing it again.", dotNetCorePlatformDetectorResult.SdkVersion);
+ return result;
}
- else
+ }
+
+ // 2. Try External-SDK (socket → blob storage)
+ if (this.commonOptions.EnableExternalSdkProvider)
+ {
+ var result = this.TryInstallFromExternalSdkProvider(sdkVersion);
+ if (result != null)
{
- if (this.commonOptions.EnableExternalSdkProvider)
- {
- this.logger.LogDebug("DotNetCore SDK version {version} is not installed. External SDK provider is enabled so trying to fetch SDK using it.", dotNetCorePlatformDetectorResult.SdkVersion);
-
- try
- {
- var blobName = BlobNameHelper.GetBlobNameForVersion(this.Name, dotNetCorePlatformDetectorResult.SdkVersion, this.commonOptions.DebianFlavor);
- var isExternalFetchSuccess = this.externalSdkProvider.RequestBlobAsync(this.Name, blobName).Result;
- if (isExternalFetchSuccess)
- {
- this.logger.LogDebug("DotNetCore SDK version {version} is fetched successfully using external SDK provider. So generating an installation script snippet which skips platform binary download.", dotNetCorePlatformDetectorResult.SdkVersion);
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(dotNetCorePlatformDetectorResult.SdkVersion, skipSdkBinaryDownload: true);
- }
- else
- {
- this.logger.LogDebug("DotNetCore SDK version {version} is not fetched successfully using external SDK provider. So generating an installation script snippet for it.", dotNetCorePlatformDetectorResult.SdkVersion);
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(dotNetCorePlatformDetectorResult.SdkVersion);
- }
- }
- catch (Exception ex)
- {
- this.logger.LogError(ex, "Error while fetching DotNetCore SDK version version {version} using external SDK provider.", dotNetCorePlatformDetectorResult.SdkVersion);
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(dotNetCorePlatformDetectorResult.SdkVersion);
- }
- }
- else
- {
- this.logger.LogDebug("DotNetCore SDK version {globalJsonSdkVersion} is not installed. So generating an installation script snippet for it.", dotNetCorePlatformDetectorResult.SdkVersion);
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(dotNetCorePlatformDetectorResult.SdkVersion);
- }
+ return result;
}
}
- else
+
+ // 3. Try Direct-ACR (direct OCI API calls)
+ if (this.commonOptions.EnableAcrSdkProvider)
{
- this.logger.LogDebug("Dynamic install is not enabled.");
+ var runtimeVersion = dotNetCorePlatformDetectorResult.PlatformVersion;
+ var result = this.TryInstallFromAcrSdkProvider(sdkVersion, runtimeVersion);
+ if (result != null)
+ {
+ return result;
+ }
}
- return installationScriptSnippet;
+ // 4. CDN fallback
+ this.outputWriter.WriteLine($"Falling back to CDN for '{this.Name}' version '{sdkVersion}'.");
+ this.logger.LogDebug(
+ "DotNetCore SDK version {globalJsonSdkVersion} is not installed. So generating an installation script snippet for it.",
+ sdkVersion);
+ return this.platformInstaller.GetInstallerScriptSnippet(sdkVersion);
}
///
@@ -295,12 +314,31 @@ public void ResolveVersions(RepositoryContext context, PlatformDetectorResult de
$"'{typeof(DotNetCorePlatformDetectorResult)}' but got '{detectorResult.GetType()}'.");
}
- // Get runtime version
+ // Resolve runtime version (same for all flows — ExternalAcrSdkProvider does not affect this).
var resolvedRuntimeVersion = this.GetRuntimeVersionUsingHierarchicalRules(
dotNetCorePlatformDetectorResult.PlatformVersion);
resolvedRuntimeVersion = this.GetMaxSatisfyingRuntimeVersionAndVerify(resolvedRuntimeVersion);
dotNetCorePlatformDetectorResult.PlatformVersion = resolvedRuntimeVersion;
+ // Resolve SDK version.
+ // External ACR provider dictates the SDK version directly — no runtime→SDK lookup needed.
+ // This is .NET-specific: other platforms have a single version,
+ // but .NET has separate runtime and SDK versions. The external host already knows
+ // which SDK companion image to use, so we trust its SDK version.
+ if (this.commonOptions.EnableExternalAcrSdkProvider)
+ {
+ var dictatedSdk = this.externalAcrVersionProvider.GetSdkVersion();
+ if (!string.IsNullOrEmpty(dictatedSdk))
+ {
+ this.logger.LogInformation(
+ "External ACR provider returned .NET SDK version {Version}. Using it directly.",
+ dictatedSdk);
+ dotNetCorePlatformDetectorResult.SdkVersion = dictatedSdk;
+ return;
+ }
+ }
+
+ // Normal SDK resolution: look up from runtime→SDK version map.
var versionMap = this.versionProvider.GetSupportedVersions();
var sdkVersion = this.GetSdkVersion(context, dotNetCorePlatformDetectorResult.PlatformVersion, versionMap);
dotNetCorePlatformDetectorResult.SdkVersion = sdkVersion;
@@ -359,6 +397,113 @@ private static void SetStartupFileNameInfoInManifestFile(
buildProperties[DotNetCoreManifestFilePropertyKeys.StartupDllFileName] = startupDllFileName;
}
+ private string TryInstallFromAcrSdkProvider(string sdkVersion, string runtimeVersion)
+ {
+ this.logger.LogDebug(
+ "DotNetCore SDK version {version} is not installed. ACR SDK provider is enabled, so trying to fetch SDK using it.",
+ sdkVersion);
+
+ try
+ {
+ var result = this.acrSdkProvider.RequestSdkFromAcrAsync(
+ this.Name, sdkVersion, this.commonOptions.DebianFlavor, runtimeVersion).Result;
+
+ if (result)
+ {
+ this.logger.LogDebug(
+ "DotNetCore SDK version {version} is fetched successfully using ACR SDK provider.",
+ sdkVersion);
+ this.outputWriter.WriteLine($"SDK for '{this.Name}' version '{sdkVersion}' fetched via direct ACR provider.");
+ return this.platformInstaller.GetInstallerScriptSnippet(sdkVersion, skipSdkBinaryDownload: true);
+ }
+
+ this.logger.LogDebug(
+ "DotNetCore SDK version {version} is not fetched via ACR SDK provider. Trying next provider.",
+ sdkVersion);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via direct ACR provider for '{this.Name}' version '{sdkVersion}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(
+ ex,
+ "Error while fetching DotNetCore SDK version {version} using ACR SDK provider. Trying next provider.",
+ sdkVersion);
+ this.outputWriter.WriteLine($"Error fetching SDK via direct ACR provider for '{this.Name}' version '{sdkVersion}'. Trying next provider.");
+ }
+
+ return null;
+ }
+
+ private string TryInstallFromExternalSdkProvider(string sdkVersion)
+ {
+ this.logger.LogDebug(
+ "DotNetCore SDK version {version} is not installed. External SDK provider is enabled so trying to fetch SDK using it.",
+ sdkVersion);
+
+ try
+ {
+ var blobName = BlobNameHelper.GetBlobNameForVersion(this.Name, sdkVersion, this.commonOptions.DebianFlavor);
+ if (this.externalSdkProvider.RequestBlobAsync(this.Name, blobName).Result)
+ {
+ this.logger.LogDebug(
+ "DotNetCore SDK version {version} is fetched successfully using external SDK provider. Skipping platform binary download.",
+ sdkVersion);
+ this.outputWriter.WriteLine($"SDK for '{this.Name}' version '{sdkVersion}' fetched via external SDK provider.");
+ return this.platformInstaller.GetInstallerScriptSnippet(sdkVersion, skipSdkBinaryDownload: true);
+ }
+
+ this.logger.LogDebug(
+ "DotNetCore SDK version {version} is not fetched successfully using external SDK provider. Generating installation script snippet.",
+ sdkVersion);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via external SDK provider for '{this.Name}' version '{sdkVersion}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(
+ ex,
+ "Error while fetching DotNetCore SDK version version {version} using external SDK provider.",
+ sdkVersion);
+ this.outputWriter.WriteLine($"Error fetching SDK via external SDK provider for '{this.Name}' version '{sdkVersion}'. Trying next provider.");
+ }
+
+ return null;
+ }
+
+ private string TryInstallFromExternalAcrSdkProvider(string sdkVersion)
+ {
+ this.logger.LogDebug(
+ "DotNetCore SDK version {version} is not installed. External ACR SDK provider is enabled, so trying to fetch SDK using it.",
+ sdkVersion);
+
+ try
+ {
+ if (this.externalAcrSdkProvider.RequestSdkAsync(
+ this.Name, sdkVersion, this.commonOptions.DebianFlavor).Result)
+ {
+ this.logger.LogDebug(
+ "DotNetCore SDK version {version} is fetched successfully using external ACR SDK provider. Skipping platform binary download.",
+ sdkVersion);
+ this.outputWriter.WriteLine($"SDK for '{this.Name}' version '{sdkVersion}' fetched via external ACR provider.");
+ return this.platformInstaller.GetInstallerScriptSnippet(sdkVersion, skipSdkBinaryDownload: true);
+ }
+
+ this.logger.LogDebug(
+ "DotNetCore SDK version {version} is not fetched via external ACR SDK provider. Trying next provider.",
+ sdkVersion);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via external ACR provider for '{this.Name}' version '{sdkVersion}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(
+ ex,
+ "Error while fetching DotNetCore SDK version {version} using external ACR SDK provider. Trying next provider.",
+ sdkVersion);
+ this.outputWriter.WriteLine($"Error fetching SDK via external ACR provider for '{this.Name}' version '{sdkVersion}'. Trying next provider.");
+ }
+
+ return null;
+ }
+
private string GetSdkVersion(
RepositoryContext context,
string runtimeVersion,
diff --git a/src/BuildScriptGenerator/DotNetCore/DotnetCoreScriptGeneratorServiceCollectionExtensions.cs b/src/BuildScriptGenerator/DotNetCore/DotnetCoreScriptGeneratorServiceCollectionExtensions.cs
index ecd68fd61c..00ae8d20aa 100644
--- a/src/BuildScriptGenerator/DotNetCore/DotnetCoreScriptGeneratorServiceCollectionExtensions.cs
+++ b/src/BuildScriptGenerator/DotNetCore/DotnetCoreScriptGeneratorServiceCollectionExtensions.cs
@@ -19,8 +19,10 @@ public static IServiceCollection AddDotNetCoreScriptGeneratorServices(this IServ
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
return services;
}
}
diff --git a/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreAcrVersionProvider.cs b/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreAcrVersionProvider.cs
new file mode 100644
index 0000000000..2f25037bff
--- /dev/null
+++ b/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreAcrVersionProvider.cs
@@ -0,0 +1,125 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator.Common;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.DotNetCore
+{
+ ///
+ /// ACR-based version provider for .NET SDKs.
+ /// Unlike other platforms, .NET requires a runtime→SDK version mapping.
+ /// This provider extracts the mapping directly from image tags, which encode
+ /// both versions in the format "{osFlavor}-{sdkVersion}_{runtimeVersion}"
+ /// (e.g. "noble-10.0.201_10.0.5").
+ ///
+ public class DotNetCoreAcrVersionProvider : AcrVersionProviderBase, IDotNetCoreVersionProvider
+ {
+ private readonly BuildScriptGeneratorOptions commonOptions;
+ private readonly ILogger logger;
+ private Dictionary versionMap;
+ private string defaultRuntimeVersion;
+
+ public DotNetCoreAcrVersionProvider(
+ IOptions commonOptions,
+ OciRegistryClient ociClient,
+ ILoggerFactory loggerFactory)
+ : base(commonOptions, ociClient, loggerFactory)
+ {
+ this.commonOptions = commonOptions.Value;
+ this.logger = loggerFactory.CreateLogger();
+ }
+
+ public string GetDefaultRuntimeVersion()
+ {
+ this.EnsureVersionInfo();
+
+ // Return null when no ACR tags matched the current OS flavor so the
+ // caller's fallback chain (External-blob → ACR → CDN) continues to the
+ // next provider. Without this, the hardcoded DefaultVersionPerFlavor
+ // value would be returned even though ACR has no matching SDKs.
+ if (this.versionMap == null || this.versionMap.Count == 0)
+ {
+ return null;
+ }
+
+ return this.defaultRuntimeVersion;
+ }
+
+ public Dictionary GetSupportedVersions()
+ {
+ this.EnsureVersionInfo();
+ if (this.versionMap == null || this.versionMap.Count == 0)
+ {
+ return null;
+ }
+
+ return this.versionMap;
+ }
+
+ private void EnsureVersionInfo()
+ {
+ if (this.versionMap != null)
+ {
+ return;
+ }
+
+ var repository = SdkImageRepositoryHelper.GetSdkImageRepository(DotNetCoreConstants.PlatformName, this.commonOptions.OryxAcrSdkRepositoryPrefix);
+ var debianFlavor = this.commonOptions.DebianFlavor;
+
+ this.GetVersionInfoFromTags(repository, debianFlavor);
+ }
+
+ ///
+ /// Parses the runtime→SDK version mapping from ACR tags.
+ /// Tags follow the format "{osFlavor}-{sdkVersion}_{runtimeVersion}".
+ ///
+ private void GetVersionInfoFromTags(string repository, string debianFlavor)
+ {
+ this.logger.LogDebug("Getting .NET version info from ACR tags for repository {repository}.", repository);
+
+ var allTags = this.OciClient.GetAllTagsAsync(repository).GetAwaiter().GetResult();
+
+ var prefix = $"{debianFlavor}-";
+ var supportedVersions = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var tag in allTags)
+ {
+ if (!tag.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ // Strip the osFlavor prefix to get "{sdkVersion}_{runtimeVersion}"
+ var versionPart = tag.Substring(prefix.Length);
+ var parts = versionPart.Split('_', 2);
+ var sdkVersion = parts[0];
+ var runtimeVersion = parts.Length > 1 ? parts[1] : null;
+ if (parts.Length != 2 || string.IsNullOrEmpty(sdkVersion) || string.IsNullOrEmpty(runtimeVersion))
+ {
+ this.logger.LogDebug("Skipping tag '{tag}' — does not match expected format.", tag);
+ continue;
+ }
+
+ supportedVersions[runtimeVersion] = sdkVersion;
+ }
+
+ this.versionMap = supportedVersions;
+
+ DotNetCoreConstants.DefaultVersionPerFlavor.TryGetValue(
+ debianFlavor ?? string.Empty, out var defaultVersion);
+ this.defaultRuntimeVersion = defaultVersion;
+
+ this.logger.LogDebug(
+ "Found {count} .NET runtime→SDK mappings from tags (default runtime: {default}).",
+ supportedVersions.Count,
+ this.defaultRuntimeVersion ?? "none");
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreExternalAcrVersionProvider.cs b/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreExternalAcrVersionProvider.cs
new file mode 100644
index 0000000000..e84443c686
--- /dev/null
+++ b/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreExternalAcrVersionProvider.cs
@@ -0,0 +1,50 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.DotNetCore
+{
+ ///
+ /// ACR-based version provider for .NET SDKs via external socket provider.
+ /// Unlike other platforms that implement I{X}VersionProvider, this exposes
+ /// a raw SDK version string. handles
+ /// the adaptation to the runtime→SDK map.
+ ///
+ public class DotNetCoreExternalAcrVersionProvider : ExternalAcrVersionProviderBase
+ {
+ private string resolvedVersion;
+ private bool resolved;
+
+ public DotNetCoreExternalAcrVersionProvider(
+ IOptions options,
+ ILoggerFactory loggerFactory,
+ IStandardOutputWriter outputWriter)
+ : base(options, loggerFactory, outputWriter)
+ {
+ }
+
+ ///
+ /// Gets the single SDK version dictated by the external host, or null if unavailable.
+ ///
+ public virtual string GetSdkVersion()
+ {
+ if (!this.resolved)
+ {
+ var flavor = this.DebianFlavor;
+ if (string.IsNullOrEmpty(flavor))
+ {
+ return null;
+ }
+
+ this.resolvedVersion = this.GetCompanionSdkVersion(DotNetCoreConstants.PlatformName, debianFlavor: flavor);
+ this.resolved = true;
+ }
+
+ return this.resolvedVersion;
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreSdkStorageVersionProvider.cs b/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreSdkStorageVersionProvider.cs
index 5724b0cc28..a8e7d6f044 100644
--- a/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreSdkStorageVersionProvider.cs
+++ b/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreSdkStorageVersionProvider.cs
@@ -70,7 +70,17 @@ public void GetVersionInfo()
{
this.logger.LogWarning(ex, "Failed to get list of blobs from primary storage. Trying backup storage.");
var sdkStorageBackupBaseUrl = this.GetPlatformBinariesBackupStorageBaseUrl();
- xdoc = ListBlobsHelper.GetAllBlobs(sdkStorageBackupBaseUrl, DotNetCoreConstants.PlatformName, httpClient);
+ if (sdkStorageBackupBaseUrl != null)
+ {
+ xdoc = ListBlobsHelper.GetAllBlobs(sdkStorageBackupBaseUrl, DotNetCoreConstants.PlatformName, httpClient);
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ $"Failed to get SDK versions for platform '{DotNetCoreConstants.PlatformName}' from primary storage URL '{sdkStorageBaseUrl}', " +
+ $"and backup storage URL is not configured. {Constants.NetworkConfigurationHelpText}",
+ ex);
+ }
}
// keys represent runtime version, values represent sdk version
diff --git a/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreVersionProvider.cs b/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreVersionProvider.cs
index 28c5236f5b..307c700d79 100644
--- a/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreVersionProvider.cs
+++ b/src/BuildScriptGenerator/DotNetCore/VersionProviders/DotNetCoreVersionProvider.cs
@@ -15,7 +15,9 @@ internal class DotNetCoreVersionProvider : IDotNetCoreVersionProvider
private readonly DotNetCoreOnDiskVersionProvider onDiskVersionProvider;
private readonly DotNetCoreSdkStorageVersionProvider sdkStorageVersionProvider;
private readonly DotNetCoreExternalVersionProvider externalVersionProvider;
+ private readonly DotNetCoreAcrVersionProvider acrVersionProvider;
private readonly ILogger logger;
+ private readonly IStandardOutputWriter outputWriter;
private string defaultRuntimeVersion;
private Dictionary supportedVersions;
@@ -24,42 +26,26 @@ public DotNetCoreVersionProvider(
DotNetCoreOnDiskVersionProvider onDiskVersionProvider,
DotNetCoreSdkStorageVersionProvider sdkStorageVersionProvider,
DotNetCoreExternalVersionProvider externalVersionProvider,
- ILogger logger)
+ DotNetCoreAcrVersionProvider acrVersionProvider,
+ ILogger logger,
+ IStandardOutputWriter outputWriter)
{
this.cliOptions = cliOptions.Value;
this.onDiskVersionProvider = onDiskVersionProvider;
this.sdkStorageVersionProvider = sdkStorageVersionProvider;
this.externalVersionProvider = externalVersionProvider;
+ this.acrVersionProvider = acrVersionProvider;
this.logger = logger;
+ this.outputWriter = outputWriter;
}
public string GetDefaultRuntimeVersion()
{
if (string.IsNullOrEmpty(this.defaultRuntimeVersion))
{
- if (this.cliOptions.EnableDynamicInstall)
- {
- if (this.cliOptions.EnableExternalSdkProvider)
- {
- try
- {
- this.defaultRuntimeVersion = this.externalVersionProvider.GetDefaultRuntimeVersion();
- }
- catch (System.Exception ex)
- {
- this.logger.LogError($"Failed to get default runtime version from external SDK provider. Falling back to http based sdkStorageVersionProvider. Ex: {ex}");
- this.defaultRuntimeVersion = this.sdkStorageVersionProvider.GetDefaultRuntimeVersion();
- }
- }
- else
- {
- this.defaultRuntimeVersion = this.sdkStorageVersionProvider.GetDefaultRuntimeVersion();
- }
- }
- else
- {
- this.defaultRuntimeVersion = this.onDiskVersionProvider.GetDefaultRuntimeVersion();
- }
+ this.defaultRuntimeVersion = this.cliOptions.EnableDynamicInstall
+ ? this.ResolveDynamicDefaultRuntimeVersion()
+ : this.onDiskVersionProvider.GetDefaultRuntimeVersion();
}
this.logger.LogDebug("Default runtime version is {defaultRuntimeVersion}", this.defaultRuntimeVersion);
@@ -71,29 +57,9 @@ public Dictionary GetSupportedVersions()
{
if (this.supportedVersions == null)
{
- if (this.cliOptions.EnableDynamicInstall)
- {
- if (this.cliOptions.EnableExternalSdkProvider)
- {
- try
- {
- this.supportedVersions = this.externalVersionProvider.GetSupportedVersions();
- }
- catch (System.Exception ex)
- {
- this.logger.LogError($"Failed to get supported versions from external SDK provider. Falling back to http based sdkStorageVersionProvider. Ex: {ex}");
- this.supportedVersions = this.sdkStorageVersionProvider.GetSupportedVersions();
- }
- }
- else
- {
- this.supportedVersions = this.sdkStorageVersionProvider.GetSupportedVersions();
- }
- }
- else
- {
- this.supportedVersions = this.onDiskVersionProvider.GetSupportedVersions();
- }
+ this.supportedVersions = this.cliOptions.EnableDynamicInstall
+ ? this.ResolveDynamicSupportedVersions()
+ : this.onDiskVersionProvider.GetSupportedVersions();
// A temporary fix to make building netcoreapp1.0 versions using the 1.1 SDK
// This SDK has 2 runtimes: 1.1.13 and 1.0.16
@@ -105,5 +71,114 @@ public Dictionary GetSupportedVersions()
return this.supportedVersions;
}
+
+ // Note: External ACR provider is handled by DotNetCorePlatform.ResolveVersions(),
+ // which overrides the SDK version(the runtime version is expected to be
+ // supplied by the caller (user-specified, detected from .csproj, or user-specified default)
+ // Priority here: External-blob → Direct-ACR → CDN
+ private string ResolveDynamicDefaultRuntimeVersion()
+ {
+ if (this.cliOptions.EnableExternalSdkProvider)
+ {
+ var version = this.TryGetDefaultRuntimeVersionFromExternalBlob();
+ if (!string.IsNullOrEmpty(version))
+ {
+ this.outputWriter.WriteLine("DotNet version resolved using external SDK provider(blob).");
+ return version;
+ }
+ }
+
+ if (this.cliOptions.EnableAcrSdkProvider)
+ {
+ var version = this.TryGetDefaultRuntimeVersionFromAcr();
+ if (!string.IsNullOrEmpty(version))
+ {
+ this.outputWriter.WriteLine("DotNet version resolved using direct ACR SDK provider.");
+ return version;
+ }
+ }
+
+ this.outputWriter.WriteLine("DotNet version resolved using blob SDK storage provider(CDN).");
+ return this.sdkStorageVersionProvider.GetDefaultRuntimeVersion();
+ }
+
+ private Dictionary ResolveDynamicSupportedVersions()
+ {
+ if (this.cliOptions.EnableExternalSdkProvider)
+ {
+ var versions = this.TryGetSupportedVersionsFromExternalBlob();
+ if (versions != null)
+ {
+ return versions;
+ }
+ }
+
+ if (this.cliOptions.EnableAcrSdkProvider)
+ {
+ var versions = this.TryGetSupportedVersionsFromAcr();
+ if (versions != null)
+ {
+ return versions;
+ }
+ }
+
+ return this.sdkStorageVersionProvider.GetSupportedVersions();
+ }
+
+ private string TryGetDefaultRuntimeVersionFromExternalBlob()
+ {
+ try
+ {
+ return this.externalVersionProvider.GetDefaultRuntimeVersion();
+ }
+ catch (System.Exception ex)
+ {
+ this.logger.LogError(
+ $"Error while getting default runtime version from external blob provider. Ex: {ex}");
+ return null;
+ }
+ }
+
+ private string TryGetDefaultRuntimeVersionFromAcr()
+ {
+ try
+ {
+ return this.acrVersionProvider.GetDefaultRuntimeVersion();
+ }
+ catch (System.Exception ex)
+ {
+ this.logger.LogError(
+ $"Error while getting default runtime version from direct ACR provider. Ex: {ex}");
+ return null;
+ }
+ }
+
+ private Dictionary TryGetSupportedVersionsFromExternalBlob()
+ {
+ try
+ {
+ return this.externalVersionProvider.GetSupportedVersions();
+ }
+ catch (System.Exception ex)
+ {
+ this.logger.LogError(
+ $"Error while getting supported versions from external blob provider. Ex: {ex}");
+ return null;
+ }
+ }
+
+ private Dictionary TryGetSupportedVersionsFromAcr()
+ {
+ try
+ {
+ return this.acrVersionProvider.GetSupportedVersions();
+ }
+ catch (System.Exception ex)
+ {
+ this.logger.LogError(
+ $"Error while getting supported versions from direct ACR provider. Ex: {ex}");
+ return null;
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/ExternalAcrSdkProvider.cs b/src/BuildScriptGenerator/ExternalAcrSdkProvider.cs
new file mode 100644
index 0000000000..b10dec60f7
--- /dev/null
+++ b/src/BuildScriptGenerator/ExternalAcrSdkProvider.cs
@@ -0,0 +1,181 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ ///
+ /// Pulls SDK tarballs from ACR via an external host over a Unix socket.
+ /// This is the ACR equivalent of (blob storage via socket).
+ ///
+ ///
+ /// Flow: Oryx → Unix socket → external host → ACR.
+ /// Connects to a dedicated ACR SDK socket and sends Action=pull-sdk so the
+ /// external host routes to the ACR image-pull logic.
+ /// Version discovery is handled by .
+ ///
+ public class ExternalAcrSdkProvider : IExternalAcrSdkProvider
+ {
+ ///
+ /// The directory where ACR-based SDKs are cached.
+ /// Must match the mount path used by the external host.
+ ///
+ public const string ExternalAcrSdksStorageDir = "/var/OryxAcrSdks";
+
+ private const string SocketPath = "/var/sdk-image-sockets/oryx-pull-sdk-image.socket";
+ private const int MaxTimeoutForSocketOperationInSeconds = 100;
+
+ private readonly ILogger logger;
+ private readonly IStandardOutputWriter outputWriter;
+
+ public ExternalAcrSdkProvider(
+ IStandardOutputWriter outputWriter,
+ ILogger logger)
+ {
+ this.logger = logger;
+ this.outputWriter = outputWriter;
+ }
+
+ ///
+ public async Task RequestSdkAsync(string platformName, string version, string debianFlavor)
+ {
+ if (string.IsNullOrEmpty(platformName))
+ {
+ throw new ArgumentException("Platform name cannot be null or empty.", nameof(platformName));
+ }
+
+ if (string.IsNullOrEmpty(version))
+ {
+ throw new ArgumentException("Version cannot be null or empty.", nameof(version));
+ }
+
+ if (string.IsNullOrEmpty(debianFlavor))
+ {
+ throw new ArgumentException("Debian flavor cannot be null or empty.", nameof(debianFlavor));
+ }
+
+ // No local cache check here — external provider handles digest-based freshness internally.
+ // On each call, the external provider does a lightweight HEAD request to compare the remote manifest for sdk image
+ // digest against a local digest file, and only re-pulls the image if the digest changed.
+ this.logger.LogInformation(
+ "Requesting SDK from ACR via external provider: platform={PlatformName}, version={Version}, " +
+ "debianFlavor={DebianFlavor}",
+ platformName,
+ version,
+ debianFlavor);
+ this.outputWriter.WriteLine(
+ $"Requesting SDK from ACR via external provider: {platformName} {version} ({debianFlavor})");
+
+ try
+ {
+ var request = new ExternalAcrSdkProviderRequest
+ {
+ Action = "pull-sdk",
+ PlatformName = platformName,
+ Version = version,
+ DebianFlavor = debianFlavor,
+ };
+
+ var responseFilename = await this.SendRequestAsync(request);
+
+ if (!string.IsNullOrEmpty(responseFilename))
+ {
+ var filePath = Path.Combine(ExternalAcrSdksStorageDir, platformName, responseFilename);
+ this.logger.LogInformation(
+ "Successfully pulled SDK from ACR via external provider: {PlatformName} {Version}, " +
+ "available at {FilePath}",
+ platformName,
+ version,
+ filePath);
+ this.outputWriter.WriteLine(
+ $"Successfully pulled SDK from ACR via external provider: {platformName} {version}");
+ return true;
+ }
+ else
+ {
+ this.logger.LogWarning(
+ "ACR SDK pull via external provider was unsuccessful: {PlatformName} {Version}",
+ platformName,
+ version);
+ this.outputWriter.WriteLine(
+ $"Failed to pull SDK from ACR via external provider: {platformName} {version}");
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(
+ ex,
+ "Error requesting SDK from ACR via external provider: {PlatformName} {Version}",
+ platformName,
+ version);
+ this.outputWriter.WriteLine(
+ $"Error pulling SDK from ACR via external provider: {platformName} {version}: {ex.Message}");
+ return false;
+ }
+ }
+
+ private async Task SendRequestAsync(ExternalAcrSdkProviderRequest request)
+ {
+ try
+ {
+ this.logger.LogInformation(
+ "Sending ACR request via socket: Action={Action}, PlatformName={PlatformName}, Version={Version}, DebianFlavor={DebianFlavor}",
+ request.Action,
+ request.PlatformName,
+ request.Version,
+ request.DebianFlavor);
+
+ var responseString = await SocketRequestHelper.SendRequestAsync(SocketPath, request, MaxTimeoutForSocketOperationInSeconds);
+ responseString = responseString?.TrimEnd('$');
+
+ this.logger.LogInformation(
+ "Received response from external ACR provider: {Response}", responseString);
+
+ if (!string.IsNullOrEmpty(responseString) &&
+ !responseString.Equals("Error", StringComparison.OrdinalIgnoreCase))
+ {
+ return responseString.Trim();
+ }
+ else
+ {
+ this.logger.LogError(
+ "ACR request via socket was unsuccessful. Response: {Response}",
+ responseString);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ this.outputWriter.WriteLine("The external ACR provider operation was canceled due to timeout.");
+ this.logger.LogError("The external ACR provider operation was canceled due to timeout.");
+ }
+ catch (Exception ex)
+ {
+ this.outputWriter.WriteLine(
+ $"Error communicating with external ACR provider: {ex.Message}");
+ this.logger.LogError(ex, "Error communicating with external ACR provider.");
+ }
+
+ return null;
+ }
+
+ private class ExternalAcrSdkProviderRequest
+ {
+ public string Action { get; set; }
+
+ public string PlatformName { get; set; }
+
+ public string Version { get; set; }
+
+ public string DebianFlavor { get; set; }
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/ExternalAcrVersionProviderBase.cs b/src/BuildScriptGenerator/ExternalAcrVersionProviderBase.cs
new file mode 100644
index 0000000000..5015271f8e
--- /dev/null
+++ b/src/BuildScriptGenerator/ExternalAcrVersionProviderBase.cs
@@ -0,0 +1,129 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ ///
+ /// Base class for version providers that resolve the companion SDK version for a platform
+ /// via a Unix socket to the external host.
+ /// The external host dictates the SDK version to use for each platform.
+ ///
+ ///
+ /// Flow: Oryx → Unix socket → external host → single SDK version response.
+ /// Connects to the dedicated ACR SDK socket and sends Action=get-version.
+ /// SDK pulling is handled separately by .
+ ///
+ public class ExternalAcrVersionProviderBase
+ {
+ private const string SocketPath = "/var/sdk-image-sockets/oryx-pull-sdk-image.socket";
+ private const int MaxTimeoutForSocketOperationInSeconds = 100;
+
+ private readonly BuildScriptGeneratorOptions commonOptions;
+ private readonly ILogger logger;
+ private readonly IStandardOutputWriter outputWriter;
+
+ public ExternalAcrVersionProviderBase(
+ IOptions options,
+ ILoggerFactory loggerFactory,
+ IStandardOutputWriter outputWriter)
+ {
+ this.commonOptions = options.Value;
+ this.logger = loggerFactory.CreateLogger(this.GetType());
+ this.outputWriter = outputWriter;
+ }
+
+ ///
+ /// Gets the Debian flavor from build options.
+ ///
+ protected string DebianFlavor => this.commonOptions.DebianFlavor;
+
+ ///
+ /// Asks the external provider for the single SDK version to use for .
+ ///
+ /// The SDK version string, or null if the external provider could not resolve one.
+ protected string GetCompanionSdkVersion(string platformName, string debianFlavor)
+ {
+ this.logger.LogInformation(
+ "Requesting companion SDK version for {PlatformName} and Debian flavor {DebianFlavor} from external ACR provider.",
+ platformName,
+ debianFlavor);
+
+ string version;
+ try
+ {
+ version = this.SendRequestAsync(platformName, debianFlavor).GetAwaiter().GetResult();
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(
+ ex,
+ "Failed to get companion SDK version for {PlatformName} and Debian flavor {DebianFlavor} from external ACR provider.",
+ platformName,
+ debianFlavor);
+ return null;
+ }
+
+ if (!string.IsNullOrEmpty(version))
+ {
+ this.logger.LogInformation(
+ "External ACR provider returned SDK version {Version} for {PlatformName} and Debian flavor {DebianFlavor}.",
+ version,
+ platformName,
+ debianFlavor);
+ this.outputWriter.WriteLine(
+ $"External ACR provider resolved version '{version}' for {platformName}.");
+ }
+ else
+ {
+ this.logger.LogWarning(
+ "External ACR provider returned no SDK version for {PlatformName} and Debian flavor {DebianFlavor}.",
+ platformName,
+ debianFlavor);
+ this.outputWriter.WriteLine(
+ $"External ACR version provider returned no version for {platformName}. Trying next provider.");
+ }
+
+ return version;
+ }
+
+ private async Task SendRequestAsync(string platformName, string debianFlavor, string action = "get-version")
+ {
+ try
+ {
+ var request = new { Action = action, PlatformName = platformName, DebianFlavor = debianFlavor };
+ var responseString = await SocketRequestHelper.SendRequestAsync(SocketPath, request, MaxTimeoutForSocketOperationInSeconds);
+ responseString = responseString?.TrimEnd('$');
+
+ if (!string.IsNullOrWhiteSpace(responseString) &&
+ !responseString.Equals("Error", StringComparison.OrdinalIgnoreCase))
+ {
+ return responseString.Trim();
+ }
+
+ this.logger.LogError(
+ "External provider returned an unsuccessful response: {Response}",
+ responseString);
+ }
+ catch (OperationCanceledException)
+ {
+ this.logger.LogError("Request to external provider timed out.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error communicating with external provider.");
+ }
+
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/ExternalSdkProvider.cs b/src/BuildScriptGenerator/ExternalSdkProvider.cs
index cfc19e0cc2..69ccf5a4de 100644
--- a/src/BuildScriptGenerator/ExternalSdkProvider.cs
+++ b/src/BuildScriptGenerator/ExternalSdkProvider.cs
@@ -7,11 +7,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Net.Sockets;
using System.Security.Cryptography;
-using System.Text;
using System.Text.Json;
-using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Xml.XPath;
@@ -207,34 +204,20 @@ public async Task RequestBlobAsync(string platformName, string blobName)
private async Task SendRequestAsync(SdkProviderRequest request)
{
- using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
try
{
this.logger.LogInformation("Sending request to external SDK provider: {PlatformName} , {BlobName}, UrlParameters: {UrlParamsJson}", request.PlatformName, request.BlobName, JsonSerializer.Serialize(request.UrlParameters));
- using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(MaxTimeoutForSocketOperationInSeconds)))
- {
- await socket.ConnectAsync(new UnixDomainSocketEndPoint(SocketPath), cts.Token);
- var requestJson = JsonSerializer.Serialize(request);
- this.logger.LogInformation("Connected to socket {socketPath} and sending request: {requestJson}", SocketPath, requestJson);
-
- // append $ at the end of the string to indicate end of request
- requestJson += "$";
- var requestBytes = Encoding.UTF8.GetBytes(requestJson);
+ var responseString = await SocketRequestHelper.SendRequestAsync(SocketPath, request, MaxTimeoutForSocketOperationInSeconds);
- await socket.SendAsync(new ArraySegment(requestBytes), SocketFlags.None, cts.Token);
- var buffer = new byte[4096];
- var received = await socket.ReceiveAsync(new ArraySegment(buffer), SocketFlags.None, cts.Token);
- var responseString = Encoding.UTF8.GetString(buffer, 0, received);
- this.logger.LogInformation("Received response from external SDK provider: {response}", responseString);
- if (!string.IsNullOrEmpty(responseString) && responseString.EqualsIgnoreCase("Success$"))
- {
- return true;
- }
- else
- {
- this.logger.LogError("Request to external SDK provider was unsuccessful. Response: {response}", responseString);
- }
+ this.logger.LogInformation("Received response from external SDK provider: {response}", responseString);
+ if (!string.IsNullOrEmpty(responseString) && responseString.EqualsIgnoreCase("Success$"))
+ {
+ return true;
+ }
+ else
+ {
+ this.logger.LogError("Request to external SDK provider was unsuccessful. Response: {response}", responseString);
}
}
catch (OperationCanceledException)
diff --git a/src/BuildScriptGenerator/ExternalSdkStorageVersionProviderBase.cs b/src/BuildScriptGenerator/ExternalSdkStorageVersionProviderBase.cs
index 2fa5c2f6cf..a15b163dfe 100644
--- a/src/BuildScriptGenerator/ExternalSdkStorageVersionProviderBase.cs
+++ b/src/BuildScriptGenerator/ExternalSdkStorageVersionProviderBase.cs
@@ -107,7 +107,7 @@ protected string GetDefaultVersion(string platformName)
while ((line = stringReader.ReadLine()) != null)
{
// Ignore any comments in the file
- if (!line.StartsWith("#") || !line.StartsWith("//"))
+ if (!line.StartsWith("#") && !line.StartsWith("//"))
{
defaultVersion = line.Trim();
break;
diff --git a/src/BuildScriptGenerator/Helpers/OciDescriptor.cs b/src/BuildScriptGenerator/Helpers/OciDescriptor.cs
new file mode 100644
index 0000000000..0c291f7b2e
--- /dev/null
+++ b/src/BuildScriptGenerator/Helpers/OciDescriptor.cs
@@ -0,0 +1,21 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ public class OciDescriptor
+ {
+ [JsonPropertyName("mediaType")]
+ public string MediaType { get; set; }
+
+ [JsonPropertyName("digest")]
+ public string Digest { get; set; }
+
+ [JsonPropertyName("size")]
+ public long Size { get; set; }
+ }
+}
diff --git a/src/BuildScriptGenerator/Helpers/OciManifest.cs b/src/BuildScriptGenerator/Helpers/OciManifest.cs
new file mode 100644
index 0000000000..36312d64cf
--- /dev/null
+++ b/src/BuildScriptGenerator/Helpers/OciManifest.cs
@@ -0,0 +1,25 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ public class OciManifest
+ {
+ [JsonPropertyName("schemaVersion")]
+ public int SchemaVersion { get; set; }
+
+ [JsonPropertyName("mediaType")]
+ public string MediaType { get; set; }
+
+ [JsonPropertyName("config")]
+ public OciDescriptor Config { get; set; }
+
+ [JsonPropertyName("layers")]
+ public List Layers { get; set; }
+ }
+}
diff --git a/src/BuildScriptGenerator/Helpers/OciRegistryClient.cs b/src/BuildScriptGenerator/Helpers/OciRegistryClient.cs
new file mode 100644
index 0000000000..b4295de0d2
--- /dev/null
+++ b/src/BuildScriptGenerator/Helpers/OciRegistryClient.cs
@@ -0,0 +1,340 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Security.Cryptography;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ ///
+ /// HTTP client for the OCI Distribution API. Enables Oryx to discover SDK versions
+ /// and download SDK tarballs from an OCI-compliant container registry (e.g. Azure Container Registry)
+ /// using only HttpClient — no external tools (docker, crane, oras) required.
+ /// Public registries still require an anonymous bearer token obtained from the registry's
+ /// OAuth2 token endpoint; this client acquires and caches tokens per repository scope.
+ ///
+ public class OciRegistryClient
+ {
+ private readonly HttpClient httpClient;
+ private readonly string registryUrl;
+ private readonly string registryHost;
+ private readonly ILogger logger;
+ private readonly ConcurrentDictionary tokenCache
+ = new ConcurrentDictionary();
+
+ public OciRegistryClient(string registryUrl, IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory)
+ {
+ if (string.IsNullOrWhiteSpace(registryUrl))
+ {
+ throw new ArgumentException("Registry URL must not be empty.", nameof(registryUrl));
+ }
+
+ var trimmed = registryUrl.TrimEnd('/');
+ if (!trimmed.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(
+ "Registry URL must use HTTPS.",
+ nameof(registryUrl));
+ }
+
+ this.registryUrl = trimmed;
+ this.registryHost = new Uri(trimmed).Host;
+ this.httpClient = httpClientFactory.CreateClient("acr");
+ this.logger = loggerFactory.CreateLogger();
+ }
+
+ ///
+ /// Gets the first layer digest from a manifest (SDK images are single-layer FROM scratch images).
+ ///
+ public static string GetFirstLayerDigest(OciManifest manifest)
+ {
+ return manifest?.Layers?.FirstOrDefault()?.Digest;
+ }
+
+ ///
+ /// Lists all tags for a repository, handling Link-header pagination.
+ ///
+ public async Task> GetAllTagsAsync(string repository)
+ {
+ var allTags = new List();
+ var url = $"{this.registryUrl}/v2/{repository}/tags/list";
+
+ while (!string.IsNullOrEmpty(url))
+ {
+ this.logger.LogDebug("Fetching tags from {url}", url);
+ using (var request = await this.CreateAuthenticatedRequestAsync(HttpMethod.Get, url, repository))
+ using (var response = await this.httpClient.SendAsync(request))
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new HttpRequestException(
+ $"Failed to list tags for repository '{repository}' (HTTP {(int)response.StatusCode}).");
+ }
+
+ var json = await response.Content.ReadAsStringAsync();
+ var tagList = JsonSerializer.Deserialize(json);
+ if (tagList?.Tags != null)
+ {
+ allTags.AddRange(tagList.Tags);
+ }
+
+ // Handle OCI pagination via Link header (RFC 5988)
+ url = null;
+ if (response.Headers.TryGetValues("Link", out var linkValues))
+ {
+ var linkHeader = linkValues.FirstOrDefault();
+ if (linkHeader != null)
+ {
+ var match = Regex.Match(linkHeader, @"<([^>]+)>;\s*rel=""next""");
+ if (match.Success)
+ {
+ url = match.Groups[1].Value;
+ if (!url.StartsWith("http"))
+ {
+ url = $"{this.registryUrl}{url}";
+ }
+ else if (!new Uri(url).Host.Equals(this.registryHost, StringComparison.OrdinalIgnoreCase))
+ {
+ this.logger.LogWarning(
+ "Ignoring Link header — URL host does not match registry host {RegistryHost}: {Url}",
+ this.registryHost,
+ url);
+ url = null;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return allTags;
+ }
+
+ ///
+ /// Fetches an OCI image manifest for the given repository and tag.
+ /// Sends both OCI and Docker v2 Accept types in a single request so the registry
+ /// can return whichever format it supports without needing a fallback round-trip.
+ ///
+ public async Task GetManifestAsync(string repository, string tag)
+ {
+ var url = $"{this.registryUrl}/v2/{repository}/manifests/{tag}";
+ using (var request = await this.CreateAuthenticatedRequestAsync(HttpMethod.Get, url, repository))
+ {
+ // Accept both formats in one request — avoids a second round-trip when
+ // the registry only supports Docker v2 (benchmarked ~50% faster on ACR).
+ request.Headers.Accept.Add(
+ new MediaTypeWithQualityHeaderValue("application/vnd.oci.image.manifest.v1+json", 1.0));
+ request.Headers.Accept.Add(
+ new MediaTypeWithQualityHeaderValue("application/vnd.docker.distribution.manifest.v2+json", 0.9));
+
+ using (var response = await this.httpClient.SendAsync(request))
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new HttpRequestException(
+ $"Failed to fetch manifest for '{repository}:{tag}' (HTTP {(int)response.StatusCode}).");
+ }
+
+ // Deserialize directly from the response stream — avoids an
+ // intermediate string allocation for the manifest JSON.
+ using (var stream = await response.Content.ReadAsStreamAsync())
+ {
+ return await JsonSerializer.DeserializeAsync(stream);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets the manifest digest for a tag via a HEAD request.
+ /// Returns the Docker-Content-Digest header value (e.g. sha256:abc...),
+ /// or null if unavailable.
+ ///
+ public async Task GetManifestDigestAsync(string repository, string tag)
+ {
+ var url = $"{this.registryUrl}/v2/{repository}/manifests/{tag}";
+ using (var request = await this.CreateAuthenticatedRequestAsync(HttpMethod.Head, url, repository))
+ {
+ request.Headers.Accept.Add(
+ new MediaTypeWithQualityHeaderValue("application/vnd.oci.image.manifest.v1+json", 1.0));
+ request.Headers.Accept.Add(
+ new MediaTypeWithQualityHeaderValue("application/vnd.docker.distribution.manifest.v2+json", 0.9));
+
+ using (var response = await this.httpClient.SendAsync(request))
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ this.logger.LogWarning(
+ "HEAD manifest failed for '{repository}:{tag}' (HTTP {statusCode}).",
+ repository,
+ tag,
+ (int)response.StatusCode);
+ return null;
+ }
+
+ if (response.Headers.TryGetValues("Docker-Content-Digest", out var values))
+ {
+ return values.FirstOrDefault();
+ }
+
+ return null;
+ }
+ }
+ }
+
+ ///
+ /// Downloads a layer blob (the SDK tarball) to disk and verifies its SHA256 digest.
+ /// The digest in the manifest IS the content hash — no separate checksum metadata needed.
+ /// Uses single-pass streaming: the SHA256 hash is computed incrementally as bytes are
+ /// written to disk, eliminating a second full read of the file for verification.
+ ///
+ public async Task DownloadLayerBlobAsync(string repository, string layerDigest, string outputPath)
+ {
+ var url = $"{this.registryUrl}/v2/{repository}/blobs/{layerDigest}";
+ this.logger.LogDebug("Downloading layer blob {digest} from {repository}", layerDigest, repository);
+
+ var expectedSha = layerDigest.StartsWith("sha256:")
+ ? layerDigest.Substring("sha256:".Length)
+ : layerDigest;
+
+ using (var request = await this.CreateAuthenticatedRequestAsync(HttpMethod.Get, url, repository))
+ {
+ using (var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new HttpRequestException(
+ $"Failed to download blob from '{repository}' (HTTP {(int)response.StatusCode}).");
+ }
+
+ using (var sha256 = IncrementalHash.CreateHash(HashAlgorithmName.SHA256))
+ using (var networkStream = await response.Content.ReadAsStreamAsync())
+ using (var fileStream = File.Create(outputPath))
+ {
+ var buffer = new byte[81920];
+ int bytesRead;
+ while ((bytesRead = await networkStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
+ {
+ sha256.AppendData(buffer, 0, bytesRead);
+ await fileStream.WriteAsync(buffer, 0, bytesRead);
+ }
+
+ var actualSha = Convert.ToHexString(sha256.GetHashAndReset()).ToLowerInvariant();
+ if (!string.Equals(actualSha, expectedSha, StringComparison.OrdinalIgnoreCase))
+ {
+ this.logger.LogError(
+ "SHA256 digest mismatch for blob {digest}. Expected: {expected}, Actual: {actual}",
+ layerDigest,
+ expectedSha,
+ actualSha);
+ fileStream.Close();
+ File.Delete(outputPath);
+ return false;
+ }
+ }
+ }
+ }
+
+ this.logger.LogDebug("Successfully downloaded and verified layer blob {digest}", layerDigest);
+ return true;
+ }
+
+ ///
+ /// Acquires an anonymous bearer token for pulling from a public repository.
+ /// Token endpoint follows the standard pattern: https://{host}/oauth2/token
+ ///
+ private async Task GetAnonymousTokenAsync(string repository)
+ {
+ // MCR registries (mcr.microsoft.com, mcr.microsoft.us, mcr.microsoft.cn, etc.)
+ // are fully public and do not expose an OAuth2 token endpoint.
+ // Skip the token flow to avoid unnecessary latency and failed requests.
+ if (this.registryHost.StartsWith("mcr.microsoft", StringComparison.OrdinalIgnoreCase))
+ {
+ this.logger.LogDebug("Skipping auth token for MCR registry {host}.", this.registryHost);
+ return null;
+ }
+
+ const int TokenRefreshBufferSeconds = 60;
+ const int DefaultTokenLifetimeSeconds = 300;
+
+ var scope = $"repository:{repository}:pull";
+ if (this.tokenCache.TryGetValue(scope, out var cached)
+ && cached.ExpiresAt > DateTimeOffset.UtcNow.AddSeconds(TokenRefreshBufferSeconds))
+ {
+ return cached.Token;
+ }
+
+ var tokenUrl = $"{this.registryUrl}/oauth2/token?service={this.registryHost}&scope={scope}";
+ this.logger.LogDebug("Requesting anonymous token for scope '{scope}'", scope);
+
+ using (var response = await this.httpClient.GetAsync(tokenUrl))
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ this.logger.LogWarning(
+ "Failed to obtain anonymous token (HTTP {statusCode}). Proceeding without auth.",
+ (int)response.StatusCode);
+ return null;
+ }
+
+ var json = await response.Content.ReadAsStringAsync();
+ using (var doc = JsonDocument.Parse(json))
+ {
+ // Token endpoints return either "access_token" or "token"
+ var root = doc.RootElement;
+ string token = null;
+ if (root.TryGetProperty("access_token", out var at))
+ {
+ token = at.GetString();
+ }
+ else if (root.TryGetProperty("token", out var t))
+ {
+ token = t.GetString();
+ }
+
+ if (!string.IsNullOrEmpty(token))
+ {
+ int expiresIn = DefaultTokenLifetimeSeconds;
+ if (root.TryGetProperty("expires_in", out var expiresInProp)
+ && expiresInProp.TryGetInt32(out var parsed))
+ {
+ expiresIn = parsed;
+ }
+
+ var expiresAt = DateTimeOffset.UtcNow.AddSeconds(expiresIn);
+ this.tokenCache[scope] = (token, expiresAt);
+ }
+
+ return token;
+ }
+ }
+ }
+
+ ///
+ /// Creates an HttpRequestMessage with the anonymous bearer token if available.
+ ///
+ private async Task CreateAuthenticatedRequestAsync(HttpMethod method, string url, string repository)
+ {
+ var request = new HttpRequestMessage(method, url);
+ var token = await this.GetAnonymousTokenAsync(repository);
+ if (!string.IsNullOrEmpty(token))
+ {
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ }
+
+ return request;
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/Helpers/OciTagList.cs b/src/BuildScriptGenerator/Helpers/OciTagList.cs
new file mode 100644
index 0000000000..261b23565b
--- /dev/null
+++ b/src/BuildScriptGenerator/Helpers/OciTagList.cs
@@ -0,0 +1,19 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ public class OciTagList
+ {
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ [JsonPropertyName("tags")]
+ public List Tags { get; set; }
+ }
+}
diff --git a/src/BuildScriptGenerator/Helpers/SocketRequestHelper.cs b/src/BuildScriptGenerator/Helpers/SocketRequestHelper.cs
new file mode 100644
index 0000000000..b76d107285
--- /dev/null
+++ b/src/BuildScriptGenerator/Helpers/SocketRequestHelper.cs
@@ -0,0 +1,70 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ ///
+ /// Handles Unix domain socket communication used by the external SDK providers.
+ /// Encapsulates the connect → send → receive loop with '$' terminator protocol.
+ ///
+ public static class SocketRequestHelper
+ {
+ private const int DefaultTimeoutSeconds = 100;
+ private const int BufferSize = 4096;
+
+ ///
+ /// Sends a JSON-serialized request to a Unix domain socket and returns the raw response.
+ /// The protocol appends '$' as a message terminator for both request and response.
+ ///
+ /// Path to the Unix domain socket.
+ /// Object to JSON-serialize and send.
+ /// Timeout for the entire operation.
+ /// The raw response string (including '$' terminator if present), or empty on EOF.
+ public static async Task SendRequestAsync(
+ string socketPath,
+ object request,
+ int timeoutSeconds = DefaultTimeoutSeconds)
+ {
+ using (var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified))
+ using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)))
+ {
+ await socket.ConnectAsync(new UnixDomainSocketEndPoint(socketPath), cts.Token);
+
+ var requestJson = JsonSerializer.Serialize(request) + "$";
+ var requestBytes = Encoding.UTF8.GetBytes(requestJson);
+
+ await socket.SendAsync(new ArraySegment(requestBytes), SocketFlags.None, cts.Token);
+
+ // Read until '$' terminator — TCP may fragment the response across multiple reads.
+ var responseBuilder = new StringBuilder();
+ var buffer = new byte[BufferSize];
+ while (true)
+ {
+ var received = await socket.ReceiveAsync(
+ new ArraySegment(buffer), SocketFlags.None, cts.Token);
+ if (received == 0)
+ {
+ break;
+ }
+
+ responseBuilder.Append(Encoding.UTF8.GetString(buffer, 0, received));
+ if (responseBuilder.Length > 0 && responseBuilder[responseBuilder.Length - 1] == '$')
+ {
+ break;
+ }
+ }
+
+ return responseBuilder.ToString();
+ }
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/Node/NodeConstants.cs b/src/BuildScriptGenerator/Node/NodeConstants.cs
index 98ca091b71..e77b4fa6da 100644
--- a/src/BuildScriptGenerator/Node/NodeConstants.cs
+++ b/src/BuildScriptGenerator/Node/NodeConstants.cs
@@ -3,6 +3,8 @@
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
+using System.Collections.Generic;
+
namespace Microsoft.Oryx.BuildScriptGenerator.Node
{
public static class NodeConstants
@@ -51,5 +53,18 @@ public static class NodeConstants
public const string InstalledNodeVersionsDir = "/opt/nodejs/";
public const string NodeVersion = "NODE_VERSION";
public const string LegacyZipNodeModules = "ENABLE_NODE_MODULES_ZIP";
+
+ ///
+ /// Default Node major version per OS flavor, matching platforms/nodejs/versions/*/defaultVersion.txt.
+ ///
+ public static readonly Dictionary DefaultVersionPerFlavor = new Dictionary
+ {
+ { "bookworm", "20" },
+ { "bullseye", "16" },
+ { "buster", "16" },
+ { "focal-scm", "16" },
+ { "noble", "24" },
+ { "stretch", "16" },
+ };
}
}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/Node/NodePlatform.cs b/src/BuildScriptGenerator/Node/NodePlatform.cs
index 87cbed0817..e949d8df87 100644
--- a/src/BuildScriptGenerator/Node/NodePlatform.cs
+++ b/src/BuildScriptGenerator/Node/NodePlatform.cs
@@ -86,7 +86,10 @@ internal class NodePlatform : IProgrammingPlatform
private readonly IEnvironment environment;
private readonly NodePlatformInstaller platformInstaller;
private readonly IExternalSdkProvider externalSdkProvider;
+ private readonly IExternalAcrSdkProvider externalAcrSdkProvider;
+ private readonly IAcrSdkProvider acrSdkProvider;
private readonly TelemetryClient telemetryClient;
+ private readonly IStandardOutputWriter outputWriter;
///
/// Initializes a new instance of the class.
@@ -108,7 +111,10 @@ public NodePlatform(
IEnvironment environment,
NodePlatformInstaller nodePlatformInstaller,
IExternalSdkProvider externalSdkProvider,
- TelemetryClient telemetryClient)
+ IExternalAcrSdkProvider externalAcrSdkProvider,
+ IAcrSdkProvider acrSdkProvider,
+ TelemetryClient telemetryClient,
+ IStandardOutputWriter outputWriter)
{
this.commonOptions = commonOptions.Value;
this.nodeScriptGeneratorOptions = nodeScriptGeneratorOptions.Value;
@@ -118,7 +124,10 @@ public NodePlatform(
this.environment = environment;
this.platformInstaller = nodePlatformInstaller;
this.externalSdkProvider = externalSdkProvider;
+ this.externalAcrSdkProvider = externalAcrSdkProvider;
+ this.acrSdkProvider = acrSdkProvider;
this.telemetryClient = telemetryClient;
+ this.outputWriter = outputWriter;
}
///
@@ -338,7 +347,7 @@ public BuildScriptSnippet GenerateBashBuildScriptSnippet(
string frameworks = string.Join(",", frameworksObj.Select(p => p.Framework).ToArray());
manifestFileProperties[ManifestFilePropertyKeys.Frameworks] = frameworks;
this.logger.LogInformation($"Detected the following frameworks: {frameworks}");
- Console.WriteLine($"Detected the following frameworks: {frameworks}");
+ this.outputWriter.WriteLine($"Detected the following frameworks: {frameworks}");
}
string compressNodeModulesCommand = null;
@@ -493,74 +502,62 @@ public string GetInstallerScriptSnippet(
BuildScriptGeneratorContext context,
PlatformDetectorResult detectorResult)
{
- string installationScriptSnippet = null;
- if (this.commonOptions.EnableDynamicInstall)
+ if (!this.commonOptions.EnableDynamicInstall)
{
- this.logger.LogDebug("Dynamic install is enabled.");
+ this.logger.LogDebug("Dynamic install not enabled.");
+ return null;
+ }
+
+ this.logger.LogDebug("Dynamic install is enabled.");
+
+ var version = detectorResult.PlatformVersion;
+
+ if (this.platformInstaller.IsVersionAlreadyInstalled(version))
+ {
+ this.logger.LogDebug(
+ "Node version {version} is already installed. So skipping installing it again.",
+ version);
+ return null;
+ }
+
+ // Priority: External-ACR → External-SDK → Direct-ACR → CDN
- if (this.platformInstaller.IsVersionAlreadyInstalled(detectorResult.PlatformVersion))
+ // 1. Try External-ACR (Oryx -> socket → ACR)
+ if (this.commonOptions.EnableExternalAcrSdkProvider)
+ {
+ var result = this.TryInstallFromExternalAcrSdkProvider(version);
+ if (result != null)
{
- this.logger.LogDebug(
- "Node version {version} is already installed. So skipping installing it again.",
- detectorResult.PlatformVersion);
+ return result;
}
- else
- {
- if (this.commonOptions.EnableExternalSdkProvider)
- {
- this.logger.LogDebug(
- "Node version {version} is not installed. " +
- "External SDK provider is enabled so trying to fetch SDK using it.",
- detectorResult.PlatformVersion);
-
- try
- {
- var blobName = BlobNameHelper.GetBlobNameForVersion(this.Name, detectorResult.PlatformVersion, this.commonOptions.DebianFlavor);
- var isExternalFetchSuccess = this.externalSdkProvider.RequestBlobAsync(this.Name, blobName).Result;
- if (isExternalFetchSuccess)
- {
- this.logger.LogDebug(
- "Node version {version} is fetched successfully using external SDK provider. " +
- "So generating an installation script snippet which skips platform binary download.",
- detectorResult.PlatformVersion);
-
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(detectorResult.PlatformVersion, skipSdkBinaryDownload: true);
- }
- else
- {
- this.logger.LogDebug(
- "Node version {version} is not fetched successfully using external SDK provider. " +
- "So generating an installation script snippet for it.",
- detectorResult.PlatformVersion);
-
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(
- detectorResult.PlatformVersion);
- }
- }
- catch (Exception ex)
- {
- this.logger.LogError(ex, "Error while fetching Node.js version {version} using external SDK provider.", detectorResult.PlatformVersion);
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(detectorResult.PlatformVersion);
- }
- }
- else
- {
- this.logger.LogDebug(
- "Node version {version} is not installed. " +
- "So generating an installation script snippet for it.",
- detectorResult.PlatformVersion);
+ }
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(
- detectorResult.PlatformVersion);
- }
+ // 2. Try External-SDK (Oryx -> socket → blob storage)
+ if (this.commonOptions.EnableExternalSdkProvider)
+ {
+ var result = this.TryInstallFromExternalSdkProvider(version);
+ if (result != null)
+ {
+ return result;
}
}
- else
+
+ // 3. Try Direct-ACR (Oryx -> direct OCI API calls)
+ if (this.commonOptions.EnableAcrSdkProvider)
{
- this.logger.LogDebug("Dynamic install not enabled.");
+ var result = this.TryInstallFromAcrSdkProvider(version);
+ if (result != null)
+ {
+ return result;
+ }
}
- return installationScriptSnippet;
+ // 4. CDN fallback
+ this.outputWriter.WriteLine($"Falling back to CDN for '{this.Name}' version '{version}'.");
+ this.logger.LogDebug(
+ "Node version {version} is not installed. So generating an installation script snippet for it.",
+ version);
+ return this.platformInstaller.GetInstallerScriptSnippet(version);
}
///
@@ -717,6 +714,113 @@ private static void GetAppOutputDirPath(dynamic packageJson, Dictionary();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
return services;
}
}
diff --git a/src/BuildScriptGenerator/Node/VersionProviders/NodeAcrVersionProvider.cs b/src/BuildScriptGenerator/Node/VersionProviders/NodeAcrVersionProvider.cs
new file mode 100644
index 0000000000..ba2e62b31f
--- /dev/null
+++ b/src/BuildScriptGenerator/Node/VersionProviders/NodeAcrVersionProvider.cs
@@ -0,0 +1,36 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System.Net.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Node
+{
+ ///
+ /// ACR-based version provider for Node.js SDKs.
+ /// Parallel to but uses OCI Distribution API.
+ ///
+ internal class NodeAcrVersionProvider : AcrVersionProviderBase, INodeVersionProvider
+ {
+ private PlatformVersionInfo platformVersionInfo;
+
+ public NodeAcrVersionProvider(
+ IOptions commonOptions,
+ OciRegistryClient ociClient,
+ ILoggerFactory loggerFactory)
+ : base(commonOptions, ociClient, loggerFactory)
+ {
+ }
+
+ public virtual PlatformVersionInfo GetVersionInfo()
+ {
+ return this.platformVersionInfo
+ ??= this.GetAvailableVersionsFromAcr(
+ platformName: "nodejs",
+ defaultVersionPerFlavor: NodeConstants.DefaultVersionPerFlavor);
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/Node/VersionProviders/NodeExternalAcrVersionProvider.cs b/src/BuildScriptGenerator/Node/VersionProviders/NodeExternalAcrVersionProvider.cs
new file mode 100644
index 0000000000..f3197422f5
--- /dev/null
+++ b/src/BuildScriptGenerator/Node/VersionProviders/NodeExternalAcrVersionProvider.cs
@@ -0,0 +1,40 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Node
+{
+ ///
+ /// ACR-based version provider for Node SDKs via external socket provider.
+ /// Parallel to (blob) and
+ /// (direct OCI).
+ ///
+ internal class NodeExternalAcrVersionProvider : ExternalAcrVersionProviderBase, INodeVersionProvider
+ {
+ public NodeExternalAcrVersionProvider(
+ IOptions options,
+ ILoggerFactory loggerFactory,
+ IStandardOutputWriter outputWriter)
+ : base(options, loggerFactory, outputWriter)
+ {
+ }
+
+ public virtual PlatformVersionInfo GetVersionInfo()
+ {
+ var version = this.GetCompanionSdkVersion(platformName: "nodejs", debianFlavor: this.DebianFlavor);
+ if (string.IsNullOrEmpty(version))
+ {
+ return null;
+ }
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(
+ supportedVersions: new[] { version },
+ defaultVersion: version);
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/Node/VersionProviders/NodeVersionProvider.cs b/src/BuildScriptGenerator/Node/VersionProviders/NodeVersionProvider.cs
index 1005176d50..e781110312 100644
--- a/src/BuildScriptGenerator/Node/VersionProviders/NodeVersionProvider.cs
+++ b/src/BuildScriptGenerator/Node/VersionProviders/NodeVersionProvider.cs
@@ -3,60 +3,47 @@
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
-using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.Oryx.BuildScriptGenerator.Node
{
- internal class NodeVersionProvider : INodeVersionProvider
+ internal class NodeVersionProvider : PlatformVersionProviderBase, INodeVersionProvider
{
- private readonly BuildScriptGeneratorOptions options;
private readonly NodeOnDiskVersionProvider onDiskVersionProvider;
private readonly NodeSdkStorageVersionProvider sdkStorageVersionProvider;
private readonly NodeExternalVersionProvider externalVersionProvider;
- private readonly ILogger logger;
- private PlatformVersionInfo versionInfo;
+ private readonly NodeExternalAcrVersionProvider externalAcrVersionProvider;
+ private readonly NodeAcrVersionProvider acrVersionProvider;
public NodeVersionProvider(
IOptions options,
NodeOnDiskVersionProvider onDiskVersionProvider,
NodeSdkStorageVersionProvider sdkStorageVersionProvider,
NodeExternalVersionProvider externalVersionProvider,
- ILogger logger)
+ NodeExternalAcrVersionProvider externalAcrVersionProvider,
+ NodeAcrVersionProvider acrVersionProvider,
+ ILogger logger,
+ IStandardOutputWriter outputWriter)
+ : base(options.Value, logger, outputWriter)
{
- this.options = options.Value;
this.onDiskVersionProvider = onDiskVersionProvider;
this.sdkStorageVersionProvider = sdkStorageVersionProvider;
this.externalVersionProvider = externalVersionProvider;
- this.logger = logger;
+ this.externalAcrVersionProvider = externalAcrVersionProvider;
+ this.acrVersionProvider = acrVersionProvider;
}
- public PlatformVersionInfo GetVersionInfo()
- {
- if (this.versionInfo == null)
- {
- if (this.options.EnableDynamicInstall)
- {
- if (this.options.EnableExternalSdkProvider)
- {
- try
- {
- return this.externalVersionProvider.GetVersionInfo();
- }
- catch (Exception ex)
- {
- this.logger.LogError($"Failed to get version info from external SDK provider. Falling back to http based sdkStorageVersionProvider. Ex: {ex}");
- }
- }
-
- return this.sdkStorageVersionProvider.GetVersionInfo();
- }
-
- this.versionInfo = this.onDiskVersionProvider.GetVersionInfo();
- }
-
- return this.versionInfo;
- }
+ protected override string PlatformName => "nodejs";
+
+ protected override PlatformVersionInfo GetOnDiskVersionInfo() => this.onDiskVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetSdkStorageVersionInfo() => this.sdkStorageVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetExternalVersionInfo() => this.externalVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetExternalAcrVersionInfo() => this.externalAcrVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetAcrVersionInfo() => this.acrVersionProvider.GetVersionInfo();
}
}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/Options/BuildScriptGeneratorOptions.cs b/src/BuildScriptGenerator/Options/BuildScriptGeneratorOptions.cs
index e0c51dd5fd..efbd7061a3 100644
--- a/src/BuildScriptGenerator/Options/BuildScriptGeneratorOptions.cs
+++ b/src/BuildScriptGenerator/Options/BuildScriptGeneratorOptions.cs
@@ -98,5 +98,29 @@ public class BuildScriptGeneratorOptions
public string ImageType { get; set; }
public bool OryxDisablePipUpgrade { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the external ACR SDK provider (socket → ACR) is enabled.
+ /// When true, Oryx will request SDKs from ACR via the external host over a Unix socket.
+ ///
+ public bool EnableExternalAcrSdkProvider { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the direct ACR SDK provider is enabled.
+ /// When true, Oryx will discover and download SDKs directly from an OCI-compliant container registry.
+ ///
+ public bool EnableAcrSdkProvider { get; set; }
+
+ ///
+ /// Gets or sets the base URL of the OCI registry hosting SDK images.
+ /// e.g. "https://mcr.microsoft.com"
+ ///
+ public string OryxAcrSdkRegistryUrl { get; set; }
+
+ ///
+ /// Gets or sets the repository prefix for SDK images in the OCI registry.
+ /// e.g. "oryx" produces images like {registry}/oryx/nodejs-sdk:{tag}.
+ ///
+ public string OryxAcrSdkRepositoryPrefix { get; set; }
}
}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/Php/PhpConstants.cs b/src/BuildScriptGenerator/Php/PhpConstants.cs
index 6cfad328ca..03c1ac7c04 100644
--- a/src/BuildScriptGenerator/Php/PhpConstants.cs
+++ b/src/BuildScriptGenerator/Php/PhpConstants.cs
@@ -3,6 +3,8 @@
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
+using System.Collections.Generic;
+
namespace Microsoft.Oryx.BuildScriptGenerator.Php
{
public static class PhpConstants
@@ -15,5 +17,31 @@ public static class PhpConstants
public const string DefaultPhpRuntimeVersion = Common.PhpVersions.Php73Version;
public const string InstalledPhpVersionsDir = "/opt/php/"; // TODO: consolidate with Dockerfile to yaml?
public const string InstalledPhpComposerVersionDir = "/opt/php-composer/";
+
+ ///
+ /// Default PHP major.minor version per OS flavor, matching platforms/php/versions/*/defaultVersion.txt.
+ ///
+ public static readonly Dictionary DefaultVersionPerFlavor = new Dictionary
+ {
+ { "bookworm", "8.3" },
+ { "bullseye", "8.0" },
+ { "buster", "8.0" },
+ { "focal-scm", "8.0" },
+ { "noble", "8.5" },
+ { "stretch", "8.0" },
+ };
+
+ ///
+ /// Default PHP Composer major version per OS flavor, matching platforms/php/composer/versions/*/defaultVersion.txt.
+ ///
+ public static readonly Dictionary ComposerDefaultVersionPerFlavor = new Dictionary
+ {
+ { "bookworm", "2" },
+ { "bullseye", "2" },
+ { "buster", "2" },
+ { "focal-scm", "2" },
+ { "noble", "2" },
+ { "stretch", "2" },
+ };
}
}
diff --git a/src/BuildScriptGenerator/Php/PhpPlatform.cs b/src/BuildScriptGenerator/Php/PhpPlatform.cs
index 2a189a077c..401d1c3efa 100644
--- a/src/BuildScriptGenerator/Php/PhpPlatform.cs
+++ b/src/BuildScriptGenerator/Php/PhpPlatform.cs
@@ -34,7 +34,10 @@ internal class PhpPlatform : IProgrammingPlatform
private readonly PhpPlatformInstaller phpInstaller;
private readonly PhpComposerInstaller phpComposerInstaller;
private readonly IExternalSdkProvider externalSdkProvider;
+ private readonly IExternalAcrSdkProvider externalAcrSdkProvider;
+ private readonly IAcrSdkProvider acrSdkProvider;
private readonly TelemetryClient telemetryClient;
+ private readonly IStandardOutputWriter outputWriter;
///
/// Initializes a new instance of the class.
@@ -57,7 +60,10 @@ public PhpPlatform(
PhpPlatformInstaller phpInstaller,
PhpComposerInstaller phpComposerInstaller,
IExternalSdkProvider externalSdkProvider,
- TelemetryClient telemetryClient)
+ IExternalAcrSdkProvider externalAcrSdkProvider,
+ IAcrSdkProvider acrSdkProvider,
+ TelemetryClient telemetryClient,
+ IStandardOutputWriter outputWriter)
{
this.phpScriptGeneratorOptions = phpScriptGeneratorOptions.Value;
this.commonOptions = commonOptions.Value;
@@ -68,7 +74,10 @@ public PhpPlatform(
this.phpInstaller = phpInstaller;
this.phpComposerInstaller = phpComposerInstaller;
this.externalSdkProvider = externalSdkProvider;
+ this.externalAcrSdkProvider = externalAcrSdkProvider;
+ this.acrSdkProvider = acrSdkProvider;
this.telemetryClient = telemetryClient;
+ this.outputWriter = outputWriter;
}
///
@@ -226,28 +235,21 @@ public string GetInstallerScriptSnippet(
$"'{typeof(PhpPlatformDetectorResult)}' but got '{detectorResult.GetType()}'.");
}
- if (this.commonOptions.EnableDynamicInstall)
+ if (!this.commonOptions.EnableDynamicInstall)
{
- this.logger.LogDebug("Dynamic install is enabled.");
+ this.logger.LogDebug("Dynamic install not enabled.");
+ return null;
+ }
- var scriptBuilder = new StringBuilder();
+ this.logger.LogDebug("Dynamic install is enabled.");
- this.InstallPhp(phpPlatformDetectorResult.PlatformVersion, scriptBuilder);
+ var scriptBuilder = new StringBuilder();
- this.InstallPhpComposer(phpPlatformDetectorResult.PhpComposerVersion, scriptBuilder);
+ this.InstallPhp(phpPlatformDetectorResult.PlatformVersion, scriptBuilder);
- if (scriptBuilder.Length == 0)
- {
- return null;
- }
+ this.InstallPhpComposer(phpPlatformDetectorResult.PhpComposerVersion, scriptBuilder);
- return scriptBuilder.ToString();
- }
- else
- {
- this.logger.LogDebug("Dynamic install not enabled.");
- return null;
- }
+ return scriptBuilder.Length == 0 ? null : scriptBuilder.ToString();
}
///
@@ -291,56 +293,54 @@ public string GetMaxSatisfyingPhpComposerVersionAndVerify(string version)
return maxSatisfyingVersion;
}
+ // Method to install PHP
+ // with priority: External-ACR → External-SDK → Direct-ACR → CDN
private void InstallPhp(string phpVersion, StringBuilder scriptBuilder)
{
- string script = null;
if (this.phpInstaller.IsVersionAlreadyInstalled(phpVersion))
{
this.logger.LogDebug("PHP version {version} is already installed. So skipping installing it again.", phpVersion);
return;
}
- else
+
+ bool phpInstalled = false;
+
+ // If external ACR provider is enabled.
+ if (this.commonOptions.EnableExternalAcrSdkProvider)
{
- if (this.commonOptions.EnableExternalSdkProvider)
- {
- this.logger.LogDebug("Php version {version} is not installed. External SDK provider is enabled so trying to fetch SDK using it.", phpVersion);
+ phpInstalled = this.TryInstallPhpUsingExternalAcrSdk(phpVersion, scriptBuilder);
+ }
- try
- {
- var blobName = BlobNameHelper.GetBlobNameForVersion("php", phpVersion, this.commonOptions.DebianFlavor);
- var isExternalFetchSuccess = this.externalSdkProvider.RequestBlobAsync(this.Name, blobName).Result;
- if (isExternalFetchSuccess)
- {
- this.logger.LogDebug("Php version {version} is fetched successfully using external SDK provider. So generating an installation script snippet which skips platform binary download.", phpVersion);
-
- script = this.phpInstaller.GetInstallerScriptSnippet(phpVersion, skipSdkBinaryDownload: true);
- }
- else
- {
- this.logger.LogDebug("Php version {version} is not fetched successfully using external SDK provider. So generating an installation script snippet for it.", phpVersion);
- script = this.phpInstaller.GetInstallerScriptSnippet(phpVersion);
- }
- }
- catch (Exception ex)
- {
- this.logger.LogError(ex, "Error while fetching php version {version} using external SDK provider.", phpVersion);
- script = this.phpInstaller.GetInstallerScriptSnippet(phpVersion);
- }
- }
- else
- {
- this.logger.LogDebug("Php version {version} is not installed. So generating an installation script snippet for it.", phpVersion);
- script = this.phpInstaller.GetInstallerScriptSnippet(phpVersion);
- }
+ // If PHP is not installed yet and external SDK provider is enabled, try it.
+ if (!phpInstalled && this.commonOptions.EnableExternalSdkProvider)
+ {
+ phpInstalled = this.TryInstallPhpExternalSdk(phpVersion, scriptBuilder);
+ }
+
+ // If PHP is still not installed and ACR SDK provider is enabled, try it.
+ if (!phpInstalled && this.commonOptions.EnableAcrSdkProvider)
+ {
+ phpInstalled = this.TryInstallPhpUsingAcrSdk(phpVersion, scriptBuilder);
+ }
+ if (!phpInstalled)
+ {
+ this.outputWriter.WriteLine($"Falling back to CDN for 'php' version '{phpVersion}'.");
+ this.logger.LogDebug("PHP version {version} is not installed. Trying to install it from CDN.", phpVersion);
+ var script = this.phpInstaller.GetInstallerScriptSnippet(phpVersion);
scriptBuilder.AppendLine(script);
}
+ else
+ {
+ this.logger.LogDebug("PHP version {version} is installed successfully using one of the providers.", phpVersion);
+ }
}
+ // Method to install PHP Composer
+ // with priority: External-ACR → External-SDK → Direct-ACR → CDN
private void InstallPhpComposer(string phpComposerVersion, StringBuilder scriptBuilder)
{
// Install PHP Composer
- string script = null;
if (string.IsNullOrEmpty(phpComposerVersion))
{
phpComposerVersion = PhpVersions.ComposerDefaultVersion;
@@ -351,49 +351,228 @@ private void InstallPhpComposer(string phpComposerVersion, StringBuilder scriptB
this.logger.LogDebug("PHP Composer version {version} is already installed. So skipping installing it again.", phpComposerVersion);
return;
}
+
+ bool composerInstalled = false;
+
+ // If external ACR provider is enabled.
+ if (this.commonOptions.EnableExternalAcrSdkProvider)
+ {
+ composerInstalled = this.TryInstallPhpComposerExternalAcr(phpComposerVersion, scriptBuilder);
+ }
+
+ // If PHP Composer is not installed yet and external SDK provider is enabled, try it.
+ if (!composerInstalled && this.commonOptions.EnableExternalSdkProvider)
+ {
+ composerInstalled = this.TryInstallPhpComposerExternalSdk(phpComposerVersion, scriptBuilder);
+ }
+
+ // If PHP Composer is still not installed and ACR SDK provider is enabled, try it.
+ if (!composerInstalled && this.commonOptions.EnableAcrSdkProvider)
+ {
+ composerInstalled = this.TryInstallPhpComposerUsingAcrSdk(phpComposerVersion, scriptBuilder);
+ }
+
+ if (!composerInstalled)
+ {
+ this.outputWriter.WriteLine($"Falling back to CDN for 'php-composer' version '{phpComposerVersion}'.");
+ this.logger.LogDebug("PHP Composer version {version} is not installed. Trying to install it from CDN.", phpComposerVersion);
+ var script = this.phpComposerInstaller.GetInstallerScriptSnippet(phpComposerVersion);
+ scriptBuilder.AppendLine(script);
+ }
else
{
- if (this.commonOptions.EnableExternalSdkProvider)
+ this.logger.LogDebug("PHP Composer version {version} is installed successfully using one of the providers. So skipping CDN installation.", phpComposerVersion);
+ }
+ }
+
+ private bool TryInstallPhpUsingAcrSdk(string phpVersion, StringBuilder scriptBuilder)
+ {
+ this.logger.LogDebug("PHP version {version} is not installed. ACR SDK provider is enabled, so trying to fetch SDK using it.", phpVersion);
+
+ try
+ {
+ var result = this.acrSdkProvider.RequestSdkFromAcrAsync(
+ "php", phpVersion, this.commonOptions.DebianFlavor).Result;
+
+ if (result)
{
- this.logger.LogDebug("Php Composer version {version} is not installed. External SDK provider is enabled so trying to fetch SDK using it.", phpComposerVersion);
+ this.logger.LogDebug("PHP version {version} is fetched successfully using ACR SDK provider.", phpVersion);
+ this.outputWriter.WriteLine($"SDK for 'php' version '{phpVersion}' fetched via direct ACR provider.");
+ scriptBuilder.AppendLine(this.phpInstaller.GetInstallerScriptSnippet(phpVersion, skipSdkBinaryDownload: true));
+ return true;
+ }
- try
- {
- var blobName = BlobNameHelper.GetBlobNameForVersion("php-composer", phpComposerVersion, this.commonOptions.DebianFlavor);
- var isExternalFetchSuccess = this.externalSdkProvider.RequestBlobAsync("php-composer", blobName).Result;
- if (isExternalFetchSuccess)
- {
- this.logger.LogDebug("Php composer version {version} is fetched successfully using external SDK provider. So generating an installation script snippet which skips platform binary download.", phpComposerVersion);
-
- script = this.phpComposerInstaller.GetInstallerScriptSnippet(phpComposerVersion, skipSdkBinaryDownload: true);
- }
- else
- {
- this.logger.LogDebug("Php comose version {version} is not fetched successfully using external SDK provider. So generating an installation script snippet for it.", phpComposerVersion);
- script = this.phpComposerInstaller.GetInstallerScriptSnippet(phpComposerVersion);
- }
- }
- catch (Exception ex)
- {
- this.logger.LogError(ex, "Error while fetching php composer version {version} using external SDK provider.", phpComposerVersion);
- script = this.phpComposerInstaller.GetInstallerScriptSnippet(phpComposerVersion);
- }
+ this.logger.LogDebug("PHP version {version} is not fetched via ACR SDK provider. Trying next provider.", phpVersion);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via direct ACR provider for 'php' version '{phpVersion}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error while fetching PHP version {version} using ACR SDK provider. Trying next provider.", phpVersion);
+ this.outputWriter.WriteLine($"Error fetching SDK via direct ACR provider for 'php' version '{phpVersion}'. Trying next provider.");
+ }
+
+ return false;
+ }
+
+ private bool TryInstallPhpComposerUsingAcrSdk(string phpComposerVersion, StringBuilder scriptBuilder)
+ {
+ if (string.IsNullOrEmpty(phpComposerVersion))
+ {
+ phpComposerVersion = PhpVersions.ComposerDefaultVersion;
+ }
+
+ this.logger.LogDebug("PHP Composer version {version} is not installed. ACR SDK provider is enabled, so trying to fetch SDK using it.", phpComposerVersion);
+
+ try
+ {
+ var result = this.acrSdkProvider.RequestSdkFromAcrAsync(
+ "php-composer", phpComposerVersion, this.commonOptions.DebianFlavor).Result;
+
+ if (result)
+ {
+ this.logger.LogDebug("PHP Composer version {version} is fetched successfully using ACR SDK provider.", phpComposerVersion);
+ this.outputWriter.WriteLine($"SDK for 'php-composer' version '{phpComposerVersion}' fetched via direct ACR provider.");
+ scriptBuilder.AppendLine(this.phpComposerInstaller.GetInstallerScriptSnippet(phpComposerVersion, skipSdkBinaryDownload: true));
+ return true;
+ }
+
+ this.logger.LogDebug("PHP Composer version {version} is not fetched via ACR SDK provider. Trying next provider.", phpComposerVersion);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via direct ACR provider for 'php-composer' version '{phpComposerVersion}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error while fetching PHP Composer version {version} using ACR SDK provider. Trying next provider.", phpComposerVersion);
+ this.outputWriter.WriteLine($"Error fetching SDK via direct ACR provider for 'php-composer' version '{phpComposerVersion}'. Trying next provider.");
+ }
+
+ return false;
+ }
+
+ private bool TryInstallPhpUsingExternalAcrSdk(string phpVersion, StringBuilder scriptBuilder)
+ {
+ this.logger.LogDebug("PHP version {version} is not installed. External ACR SDK provider is enabled, so trying to fetch SDK using it.", phpVersion);
+
+ try
+ {
+ if (this.externalAcrSdkProvider.RequestSdkAsync(
+ "php", phpVersion, this.commonOptions.DebianFlavor).Result)
+ {
+ this.logger.LogDebug("PHP version {version} is fetched successfully using external ACR SDK provider. Skipping platform binary download.", phpVersion);
+ this.outputWriter.WriteLine($"SDK for 'php' version '{phpVersion}' fetched via external ACR provider.");
+ scriptBuilder.AppendLine(this.phpInstaller.GetInstallerScriptSnippet(phpVersion, skipSdkBinaryDownload: true));
+ return true;
}
- else
+
+ this.logger.LogDebug("PHP version {version} is not fetched via external ACR SDK provider. Trying next provider.", phpVersion);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via external ACR provider for 'php' version '{phpVersion}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error while fetching PHP version {version} using external ACR SDK provider. Trying next provider.", phpVersion);
+ this.outputWriter.WriteLine($"Error fetching SDK via external ACR provider for 'php' version '{phpVersion}'. Trying next provider.");
+ }
+
+ return false;
+ }
+
+ private bool TryInstallPhpComposerExternalAcr(string phpComposerVersion, StringBuilder scriptBuilder)
+ {
+ if (string.IsNullOrEmpty(phpComposerVersion))
+ {
+ phpComposerVersion = PhpVersions.ComposerDefaultVersion;
+ }
+
+ this.logger.LogDebug("PHP Composer version {version} is not installed. External ACR SDK provider is enabled, so trying to fetch SDK using it.", phpComposerVersion);
+
+ try
+ {
+ if (this.externalAcrSdkProvider.RequestSdkAsync(
+ "php-composer", phpComposerVersion, this.commonOptions.DebianFlavor).Result)
{
- this.logger.LogDebug("Php composer version {version} is not installed. So generating an installation script snippet for it.", phpComposerVersion);
- script = this.phpComposerInstaller.GetInstallerScriptSnippet(phpComposerVersion);
+ this.logger.LogDebug("PHP Composer version {version} is fetched successfully using external ACR SDK provider. Skipping platform binary download.", phpComposerVersion);
+ this.outputWriter.WriteLine($"SDK for 'php-composer' version '{phpComposerVersion}' fetched via external ACR provider.");
+ scriptBuilder.AppendLine(this.phpComposerInstaller.GetInstallerScriptSnippet(phpComposerVersion, skipSdkBinaryDownload: true));
+ return true;
}
+
+ this.logger.LogDebug("PHP Composer version {version} is not fetched via external ACR SDK provider. Trying next provider.", phpComposerVersion);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via external ACR provider for 'php-composer' version '{phpComposerVersion}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error while fetching PHP Composer version {version} using external ACR SDK provider. Trying next provider.", phpComposerVersion);
+ this.outputWriter.WriteLine($"Error fetching SDK via external ACR provider for 'php-composer' version '{phpComposerVersion}'. Trying next provider.");
}
- scriptBuilder.AppendLine(script);
+ return false;
+ }
+
+ private bool TryInstallPhpExternalSdk(string phpVersion, StringBuilder scriptBuilder)
+ {
+ this.logger.LogDebug("PHP version {version} is not installed. External SDK provider is enabled, so trying to fetch SDK using it.", phpVersion);
+
+ try
+ {
+ var blobName = BlobNameHelper.GetBlobNameForVersion("php", phpVersion, this.commonOptions.DebianFlavor);
+ if (this.externalSdkProvider.RequestBlobAsync(this.Name, blobName).Result)
+ {
+ this.logger.LogDebug("PHP version {version} is fetched successfully using external SDK provider. Skipping platform binary download.", phpVersion);
+ this.outputWriter.WriteLine($"SDK for 'php' version '{phpVersion}' fetched via external SDK provider.");
+ scriptBuilder.AppendLine(this.phpInstaller.GetInstallerScriptSnippet(phpVersion, skipSdkBinaryDownload: true));
+ return true;
+ }
+
+ this.logger.LogDebug("PHP version {version} is not fetched successfully using external SDK provider. Trying next provider.", phpVersion);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via external SDK provider for 'php' version '{phpVersion}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error while fetching PHP version {version} using external SDK provider. Trying next provider.", phpVersion);
+ this.outputWriter.WriteLine($"Error fetching SDK via external SDK provider for 'php' version '{phpVersion}'. Trying next provider.");
+ }
+
+ return false;
+ }
+
+ private bool TryInstallPhpComposerExternalSdk(string phpComposerVersion, StringBuilder scriptBuilder)
+ {
+ if (string.IsNullOrEmpty(phpComposerVersion))
+ {
+ phpComposerVersion = PhpVersions.ComposerDefaultVersion;
+ }
+
+ this.logger.LogDebug("PHP Composer version {version} is not installed. External SDK provider is enabled, so trying to fetch SDK using it.", phpComposerVersion);
+
+ try
+ {
+ var blobName = BlobNameHelper.GetBlobNameForVersion("php-composer", phpComposerVersion, this.commonOptions.DebianFlavor);
+ if (this.externalSdkProvider.RequestBlobAsync("php-composer", blobName).Result)
+ {
+ this.logger.LogDebug("PHP Composer version {version} is fetched successfully using external SDK provider. Skipping platform binary download.", phpComposerVersion);
+ this.outputWriter.WriteLine($"SDK for 'php-composer' version '{phpComposerVersion}' fetched via external SDK provider.");
+ scriptBuilder.AppendLine(this.phpComposerInstaller.GetInstallerScriptSnippet(phpComposerVersion, skipSdkBinaryDownload: true));
+ return true;
+ }
+
+ this.logger.LogDebug("PHP Composer version {version} is not fetched successfully using external SDK provider. Trying next provider.", phpComposerVersion);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via external SDK provider for 'php-composer' version '{phpComposerVersion}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error while fetching PHP Composer version {version} using external SDK provider. Trying next provider.", phpComposerVersion);
+ this.outputWriter.WriteLine($"Error fetching SDK via external SDK provider for 'php-composer' version '{phpComposerVersion}'. Trying next provider.");
+ }
+
+ return false;
}
private void ResolveVersionsUsingHierarchicalRules(PhpPlatformDetectorResult detectorResult)
{
+ // Resolve PHP version using hierarchical rules
var phpVersion = ResolvePhpVersion(detectorResult.PlatformVersion);
phpVersion = this.GetMaxSatisfyingPhpVersionAndVerify(phpVersion);
+ // Resolve PHP Composer version using hierarchical rules
var phpComposerVersion = ResolvePhpComposerVersion(detectorResult.PhpComposerVersion);
phpComposerVersion = this.GetMaxSatisfyingPhpComposerVersionAndVerify(phpComposerVersion);
@@ -402,6 +581,17 @@ private void ResolveVersionsUsingHierarchicalRules(PhpPlatformDetectorResult det
string ResolvePhpVersion(string detectedVersion)
{
+ // If external ACR provider is enabled, try to resolve version from it first.
+ if (this.commonOptions.EnableExternalAcrSdkProvider)
+ {
+ var acrVersionInfo = this.phpVersionProvider.GetVersionInfo();
+ if (acrVersionInfo?.DefaultVersion != null)
+ {
+ this.logger.LogDebug("External ACR SDK provider is enabled and returned version {version} for PHP.", acrVersionInfo.DefaultVersion);
+ return acrVersionInfo.DefaultVersion;
+ }
+ }
+
// Explicitly specified version by user wins over detected version
if (!string.IsNullOrEmpty(this.phpScriptGeneratorOptions.PhpVersion))
{
@@ -427,6 +617,19 @@ string ResolvePhpVersion(string detectedVersion)
string ResolvePhpComposerVersion(string detectedVersion)
{
+ // If external ACR provider is enabled, resolve version from the composer version provider.
+ // Note: Unlike PHP, Composer versions are hardcoded locally in PhpVersions.ComposerVersionsPerDebianFlavor
+ // rather than fetched from the external host. The provider still uses the same pattern for consistency.
+ if (this.commonOptions.EnableExternalAcrSdkProvider)
+ {
+ var acrVersionInfo = this.phpComposerVersionProvider.GetVersionInfo();
+ if (acrVersionInfo?.DefaultVersion != null)
+ {
+ this.logger.LogDebug("External ACR SDK provider is enabled and returned version {version} for PHP Composer.", acrVersionInfo.DefaultVersion);
+ return acrVersionInfo.DefaultVersion;
+ }
+ }
+
// Explicitly specified version by user wins over detected version
if (!string.IsNullOrEmpty(this.phpScriptGeneratorOptions.PhpComposerVersion))
{
diff --git a/src/BuildScriptGenerator/Php/PhpScriptGeneratorServiceCollectionExtensions.cs b/src/BuildScriptGenerator/Php/PhpScriptGeneratorServiceCollectionExtensions.cs
index fe96ac23f8..9a40abf620 100644
--- a/src/BuildScriptGenerator/Php/PhpScriptGeneratorServiceCollectionExtensions.cs
+++ b/src/BuildScriptGenerator/Php/PhpScriptGeneratorServiceCollectionExtensions.cs
@@ -22,8 +22,12 @@ public static IServiceCollection AddPhpScriptGeneratorServices(this IServiceColl
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
return services;
}
}
diff --git a/src/BuildScriptGenerator/Php/VersionProviders/PhpAcrVersionProvider.cs b/src/BuildScriptGenerator/Php/VersionProviders/PhpAcrVersionProvider.cs
new file mode 100644
index 0000000000..1444211114
--- /dev/null
+++ b/src/BuildScriptGenerator/Php/VersionProviders/PhpAcrVersionProvider.cs
@@ -0,0 +1,36 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System.Net.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Php
+{
+ ///
+ /// ACR-based version provider for PHP SDKs.
+ /// Parallel to but uses OCI Distribution API.
+ ///
+ internal class PhpAcrVersionProvider : AcrVersionProviderBase, IPhpVersionProvider
+ {
+ private PlatformVersionInfo platformVersionInfo;
+
+ public PhpAcrVersionProvider(
+ IOptions commonOptions,
+ OciRegistryClient ociClient,
+ ILoggerFactory loggerFactory)
+ : base(commonOptions, ociClient, loggerFactory)
+ {
+ }
+
+ public virtual PlatformVersionInfo GetVersionInfo()
+ {
+ return this.platformVersionInfo
+ ??= this.GetAvailableVersionsFromAcr(
+ platformName: ToolNameConstants.PhpName,
+ defaultVersionPerFlavor: PhpConstants.DefaultVersionPerFlavor);
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/Php/VersionProviders/PhpComposerAcrVersionProvider.cs b/src/BuildScriptGenerator/Php/VersionProviders/PhpComposerAcrVersionProvider.cs
new file mode 100644
index 0000000000..c29a364a64
--- /dev/null
+++ b/src/BuildScriptGenerator/Php/VersionProviders/PhpComposerAcrVersionProvider.cs
@@ -0,0 +1,36 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System.Net.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Php
+{
+ ///
+ /// ACR-based version provider for PHP Composer SDKs.
+ /// Parallel to but uses OCI Distribution API.
+ ///
+ internal class PhpComposerAcrVersionProvider : AcrVersionProviderBase, IPhpComposerVersionProvider
+ {
+ private PlatformVersionInfo platformVersionInfo;
+
+ public PhpComposerAcrVersionProvider(
+ IOptions commonOptions,
+ OciRegistryClient ociClient,
+ ILoggerFactory loggerFactory)
+ : base(commonOptions, ociClient, loggerFactory)
+ {
+ }
+
+ public virtual PlatformVersionInfo GetVersionInfo()
+ {
+ return this.platformVersionInfo
+ ??= this.GetAvailableVersionsFromAcr(
+ platformName: "php-composer",
+ defaultVersionPerFlavor: PhpConstants.ComposerDefaultVersionPerFlavor);
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/Php/VersionProviders/PhpComposerExternalAcrVersionProvider.cs b/src/BuildScriptGenerator/Php/VersionProviders/PhpComposerExternalAcrVersionProvider.cs
new file mode 100644
index 0000000000..fd7953c09c
--- /dev/null
+++ b/src/BuildScriptGenerator/Php/VersionProviders/PhpComposerExternalAcrVersionProvider.cs
@@ -0,0 +1,45 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator.Common;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Php
+{
+ ///
+ /// ACR-based version provider for PHP Composer SDKs via external socket provider.
+ /// Parallel to (blob) and
+ /// (direct OCI).
+ ///
+ internal class PhpComposerExternalAcrVersionProvider : ExternalAcrVersionProviderBase, IPhpComposerVersionProvider
+ {
+ public PhpComposerExternalAcrVersionProvider(
+ IOptions options,
+ ILoggerFactory loggerFactory,
+ IStandardOutputWriter outputWriter)
+ : base(options, loggerFactory, outputWriter)
+ {
+ }
+
+ public virtual PlatformVersionInfo GetVersionInfo()
+ {
+ var availableVersions = PhpVersions.ComposerVersionsPerDebianFlavor.TryGetValue(this.DebianFlavor, out var versions)
+ ? versions
+ : null;
+
+ if (availableVersions == null || availableVersions.Length == 0)
+ {
+ return null;
+ }
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(
+ supportedVersions: availableVersions.ToArray(),
+ defaultVersion: PhpVersions.ComposerDefaultVersion);
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/Php/VersionProviders/PhpComposerVersionProvider.cs b/src/BuildScriptGenerator/Php/VersionProviders/PhpComposerVersionProvider.cs
index dd2334be23..f341bf9280 100644
--- a/src/BuildScriptGenerator/Php/VersionProviders/PhpComposerVersionProvider.cs
+++ b/src/BuildScriptGenerator/Php/VersionProviders/PhpComposerVersionProvider.cs
@@ -3,60 +3,47 @@
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
-using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.Oryx.BuildScriptGenerator.Php
{
- internal class PhpComposerVersionProvider : IPhpComposerVersionProvider
+ internal class PhpComposerVersionProvider : PlatformVersionProviderBase, IPhpComposerVersionProvider
{
- private readonly BuildScriptGeneratorOptions options;
private readonly PhpComposerOnDiskVersionProvider onDiskVersionProvider;
private readonly PhpComposerSdkStorageVersionProvider sdkStorageVersionProvider;
private readonly PhpComposerExternalVersionProvider externalVersionProvider;
- private readonly ILogger logger;
- private PlatformVersionInfo versionInfo;
+ private readonly PhpComposerExternalAcrVersionProvider externalAcrVersionProvider;
+ private readonly PhpComposerAcrVersionProvider acrVersionProvider;
public PhpComposerVersionProvider(
IOptions options,
PhpComposerOnDiskVersionProvider onDiskVersionProvider,
PhpComposerSdkStorageVersionProvider sdkStorageVersionProvider,
PhpComposerExternalVersionProvider externalVersionProvider,
- ILogger logger)
+ PhpComposerExternalAcrVersionProvider externalAcrVersionProvider,
+ PhpComposerAcrVersionProvider acrVersionProvider,
+ ILogger logger,
+ IStandardOutputWriter outputWriter)
+ : base(options.Value, logger, outputWriter)
{
- this.options = options.Value;
this.onDiskVersionProvider = onDiskVersionProvider;
this.sdkStorageVersionProvider = sdkStorageVersionProvider;
this.externalVersionProvider = externalVersionProvider;
- this.logger = logger;
+ this.externalAcrVersionProvider = externalAcrVersionProvider;
+ this.acrVersionProvider = acrVersionProvider;
}
- public PlatformVersionInfo GetVersionInfo()
- {
- if (this.versionInfo == null)
- {
- if (this.options.EnableDynamicInstall)
- {
- if (this.options.EnableExternalSdkProvider)
- {
- try
- {
- return this.externalVersionProvider.GetVersionInfo();
- }
- catch (Exception ex)
- {
- this.logger.LogError($"Failed to get version info from external SDK provider. Falling back to http based sdkStorageVersionProvider. Ex: {ex}");
- }
- }
-
- return this.sdkStorageVersionProvider.GetVersionInfo();
- }
-
- this.versionInfo = this.onDiskVersionProvider.GetVersionInfo();
- }
-
- return this.versionInfo;
- }
+ protected override string PlatformName => "php-composer";
+
+ protected override PlatformVersionInfo GetOnDiskVersionInfo() => this.onDiskVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetSdkStorageVersionInfo() => this.sdkStorageVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetExternalVersionInfo() => this.externalVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetExternalAcrVersionInfo() => this.externalAcrVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetAcrVersionInfo() => this.acrVersionProvider.GetVersionInfo();
}
}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/Php/VersionProviders/PhpExternalAcrVersionProvider.cs b/src/BuildScriptGenerator/Php/VersionProviders/PhpExternalAcrVersionProvider.cs
new file mode 100644
index 0000000000..006b59859d
--- /dev/null
+++ b/src/BuildScriptGenerator/Php/VersionProviders/PhpExternalAcrVersionProvider.cs
@@ -0,0 +1,40 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Php
+{
+ ///
+ /// ACR-based version provider for PHP SDKs via external socket provider.
+ /// Parallel to (blob) and
+ /// (direct OCI).
+ ///
+ internal class PhpExternalAcrVersionProvider : ExternalAcrVersionProviderBase, IPhpVersionProvider
+ {
+ public PhpExternalAcrVersionProvider(
+ IOptions options,
+ ILoggerFactory loggerFactory,
+ IStandardOutputWriter outputWriter)
+ : base(options, loggerFactory, outputWriter)
+ {
+ }
+
+ public virtual PlatformVersionInfo GetVersionInfo()
+ {
+ var version = this.GetCompanionSdkVersion(platformName: ToolNameConstants.PhpName, debianFlavor: this.DebianFlavor);
+ if (string.IsNullOrEmpty(version))
+ {
+ return null;
+ }
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(
+ supportedVersions: new[] { version },
+ defaultVersion: version);
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/Php/VersionProviders/PhpVersionProvider.cs b/src/BuildScriptGenerator/Php/VersionProviders/PhpVersionProvider.cs
index 991371670d..a22cbea396 100644
--- a/src/BuildScriptGenerator/Php/VersionProviders/PhpVersionProvider.cs
+++ b/src/BuildScriptGenerator/Php/VersionProviders/PhpVersionProvider.cs
@@ -3,60 +3,47 @@
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
-using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.Oryx.BuildScriptGenerator.Php
{
- internal class PhpVersionProvider : IPhpVersionProvider
+ internal class PhpVersionProvider : PlatformVersionProviderBase, IPhpVersionProvider
{
- private readonly BuildScriptGeneratorOptions options;
private readonly PhpOnDiskVersionProvider onDiskVersionProvider;
private readonly PhpSdkStorageVersionProvider sdkStorageVersionProvider;
private readonly PhpExternalVersionProvider externalVersionProvider;
- private readonly ILogger logger;
- private PlatformVersionInfo versionInfo;
+ private readonly PhpExternalAcrVersionProvider externalAcrVersionProvider;
+ private readonly PhpAcrVersionProvider acrVersionProvider;
public PhpVersionProvider(
IOptions options,
PhpOnDiskVersionProvider onDiskVersionProvider,
PhpSdkStorageVersionProvider sdkStorageVersionProvider,
PhpExternalVersionProvider externalVersionProvider,
- ILogger logger)
+ PhpExternalAcrVersionProvider externalAcrVersionProvider,
+ PhpAcrVersionProvider acrVersionProvider,
+ ILogger logger,
+ IStandardOutputWriter outputWriter)
+ : base(options.Value, logger, outputWriter)
{
- this.options = options.Value;
this.onDiskVersionProvider = onDiskVersionProvider;
this.sdkStorageVersionProvider = sdkStorageVersionProvider;
this.externalVersionProvider = externalVersionProvider;
- this.logger = logger;
+ this.externalAcrVersionProvider = externalAcrVersionProvider;
+ this.acrVersionProvider = acrVersionProvider;
}
- public PlatformVersionInfo GetVersionInfo()
- {
- if (this.versionInfo == null)
- {
- if (this.options.EnableDynamicInstall)
- {
- if (this.options.EnableExternalSdkProvider)
- {
- try
- {
- return this.externalVersionProvider.GetVersionInfo();
- }
- catch (Exception ex)
- {
- this.logger.LogError($"Failed to get version info from external SDK provider. Falling back to http based sdkStorageVersionProvider. Ex: {ex}");
- }
- }
-
- return this.sdkStorageVersionProvider.GetVersionInfo();
- }
-
- this.versionInfo = this.onDiskVersionProvider.GetVersionInfo();
- }
-
- return this.versionInfo;
- }
+ protected override string PlatformName => "php";
+
+ protected override PlatformVersionInfo GetOnDiskVersionInfo() => this.onDiskVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetSdkStorageVersionInfo() => this.sdkStorageVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetExternalVersionInfo() => this.externalVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetExternalAcrVersionInfo() => this.externalAcrVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetAcrVersionInfo() => this.acrVersionProvider.GetVersionInfo();
}
}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/PhpVersions.cs b/src/BuildScriptGenerator/PhpVersions.cs
index ceedfc1cda..99f3e11822 100644
--- a/src/BuildScriptGenerator/PhpVersions.cs
+++ b/src/BuildScriptGenerator/PhpVersions.cs
@@ -55,5 +55,13 @@ public static class PhpVersions
public const string Php56TarSha256 = "1369a51eee3995d7fbd1c5342e5cc917760e276d561595b6052b21ace2656d1c";
public static readonly List RuntimeVersions = new List { "7.4-debian-bullseye", "7.4-debian-buster", "8.0-debian-bullseye", "8.0-debian-buster", "8.1-debian-bullseye", "8.1-debian-buster", "8.2-debian-bullseye", "8.2-debian-buster", "8.3-debian-bullseye", "8.3-debian-buster", "8.3-debian-bookworm" };
public static readonly List FpmRuntimeVersions = new List { "7.4-fpm-debian-bullseye", "8.0-fpm-debian-bullseye", "8.1-fpm-debian-bullseye", "8.2-fpm-debian-bullseye", "8.3-fpm-debian-bullseye", "8.3-fpm-debian-bookworm", "8.4-fpm-debian-bullseye", "8.4-fpm-debian-bookworm", "8.5-fpm-ubuntu-noble" };
+
+ public static readonly Dictionary ComposerVersionsPerDebianFlavor = new Dictionary
+ {
+ { "bullseye", new[] { "1.9.2", "1.9.3", "1.10.0", "1.10.1", "1.10.2", "1.10.4", "1.10.5", "1.10.6", "1.10.7", "1.10.8", "1.10.9", "1.10.10", "1.10.11", "1.10.12", "1.10.13", "1.10.14", "1.10.15", "1.10.16", "1.10.17", "1.10.18", "1.10.19", "2.0.0", "2.0.1", "2.0.2", "2.0.3", "2.0.4", "2.0.5", "2.0.6", "2.0.7", "2.0.8", "2.2.9", "2.2.21", "2.3.4", "2.3.10", "2.4.4", "2.5.8", "2.6.2", "2.7.7", "2.8.2", "2.8.4", "2.8.6", "2.8.8" } },
+ { "buster", new[] { "1.9.2", "1.9.3", "1.10.0", "1.10.1", "1.10.2", "1.10.4", "1.10.5", "1.10.6", "1.10.7", "1.10.8", "1.10.9", "1.10.10", "1.10.11", "1.10.12", "1.10.13", "1.10.14", "1.10.15", "1.10.16", "1.10.17", "1.10.18", "1.10.19", "2.0.0", "2.0.1", "2.0.2", "2.0.3", "2.0.4", "2.0.5", "2.0.6", "2.0.7", "2.0.8", "2.2.9", "2.2.21", "2.3.4", "2.3.10", "2.4.4", "2.5.8", "2.6.2" } },
+ { "bookworm", new[] { "2.0.8", "2.6.2", "2.7.7", "2.8.2", "2.8.4", "2.8.6", "2.8.8" } },
+ { "noble", new[] { "2.6.2", "2.7.7", "2.8.2", "2.8.4", "2.8.6", "2.8.8" } },
+ };
}
}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/PlatformInstallerBase.cs b/src/BuildScriptGenerator/PlatformInstallerBase.cs
index 21fa5aee04..1603475663 100644
--- a/src/BuildScriptGenerator/PlatformInstallerBase.cs
+++ b/src/BuildScriptGenerator/PlatformInstallerBase.cs
@@ -146,13 +146,30 @@ protected string GetInstallerScriptSnippet(
if (skipSdkBinaryDownload)
{
+ // The tarball was pre-downloaded by an external or ACR SDK provider.
+ // Check the external blob cache (/var/OryxSdks) first, then the external
+ // ACR cache (/var/OryxAcrSdks), then the writable dynamic install dir.
var tarFileName = BlobNameHelper.GetBlobNameForVersion(platformName, version, this.CommonOptions.DebianFlavor);
- var tarFilePath = Path.Combine(ExternalSdkProvider.ExternalSdksStorageDir, platformName, tarFileName);
- snippet.AppendLine($"echo \"Skipping download of {platformName} version {version} as it is available in external sdk provider cache...\"")
- .AppendLine($"echo \"Extracting contents...\"")
- .AppendLine($"tar -xzf {tarFilePath} -C .")
- .AppendLine($"rm -f {tarFileName}")
- .AppendLine($"echo \"Successfully extracted {platformName} version {version} from external sdk provider cache...\"");
+ var externalPath = Path.Combine(ExternalSdkProvider.ExternalSdksStorageDir, platformName, tarFileName);
+ var externalAcrPath = Path.Combine(ExternalAcrSdkProvider.ExternalAcrSdksStorageDir, platformName, tarFileName);
+ var dynamicPath = Path.Combine(this.CommonOptions.DynamicInstallRootDir, platformName, tarFileName);
+
+ snippet.AppendLine($"echo \"SDK binary download was skipped. Looking for cached tarball...\"")
+ .AppendLine($"if [ -f \"{externalPath}\" ]; then")
+ .AppendLine($" echo \"Found tarball at {externalPath}\"")
+ .AppendLine($" tar -xzf {externalPath} -C .")
+ .AppendLine($"elif [ -f \"{externalAcrPath}\" ]; then")
+ .AppendLine($" echo \"Found tarball at {externalAcrPath}\"")
+ .AppendLine($" tar -xzf {externalAcrPath} -C .")
+ .AppendLine($"elif [ -f \"{dynamicPath}\" ]; then")
+ .AppendLine($" echo \"Found tarball at {dynamicPath}\"")
+ .AppendLine($" tar -xzf {dynamicPath} -C .")
+ .AppendLine($" rm -f {dynamicPath}")
+ .AppendLine($"else")
+ .AppendLine($" echo \"ERROR: Could not find cached tarball for {platformName} {version}\"")
+ .AppendLine($" exit 1")
+ .AppendLine($"fi")
+ .AppendLine($"echo \"Successfully extracted {platformName} version {version} from cached tarball.\"");
}
else
{
diff --git a/src/BuildScriptGenerator/PlatformVersionInfo.cs b/src/BuildScriptGenerator/PlatformVersionInfo.cs
index fb40ecc3b0..5a1b1700fb 100644
--- a/src/BuildScriptGenerator/PlatformVersionInfo.cs
+++ b/src/BuildScriptGenerator/PlatformVersionInfo.cs
@@ -49,5 +49,17 @@ public static PlatformVersionInfo CreateAvailableViaExternalProvider(
PlatformVersionSourceType = PlatformVersionSourceType.AvailableViaExternalProvider,
};
}
+
+ public static PlatformVersionInfo CreateAvailableOnAcr(
+ IEnumerable supportedVersions,
+ string defaultVersion)
+ {
+ return new PlatformVersionInfo
+ {
+ SupportedVersions = supportedVersions,
+ DefaultVersion = defaultVersion,
+ PlatformVersionSourceType = PlatformVersionSourceType.AvailableOnAcr,
+ };
+ }
}
}
diff --git a/src/BuildScriptGenerator/PlatformVersionProviderBase.cs b/src/BuildScriptGenerator/PlatformVersionProviderBase.cs
new file mode 100644
index 0000000000..b907270c1c
--- /dev/null
+++ b/src/BuildScriptGenerator/PlatformVersionProviderBase.cs
@@ -0,0 +1,133 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Oryx.BuildScriptGenerator
+{
+ ///
+ /// Base class for platform version providers that implements the 4-tier fallback chain:
+ /// External ACR → External SDK → Direct ACR → CDN (blob storage).
+ /// Used by Node.js, Python, PHP, and PHP Composer version providers.
+ ///
+ internal abstract class PlatformVersionProviderBase
+ {
+ private readonly BuildScriptGeneratorOptions options;
+ private readonly ILogger logger;
+ private readonly IStandardOutputWriter outputWriter;
+ private PlatformVersionInfo versionInfo;
+
+ protected PlatformVersionProviderBase(
+ BuildScriptGeneratorOptions options,
+ ILogger logger,
+ IStandardOutputWriter outputWriter)
+ {
+ this.options = options;
+ this.logger = logger;
+ this.outputWriter = outputWriter;
+ }
+
+ protected abstract string PlatformName { get; }
+
+ public PlatformVersionInfo GetVersionInfo()
+ {
+ if (this.versionInfo != null)
+ {
+ return this.versionInfo;
+ }
+
+ this.versionInfo = this.options.EnableDynamicInstall
+ ? this.ResolveDynamicVersionInfo()
+ : this.GetOnDiskVersionInfo();
+
+ return this.versionInfo;
+ }
+
+ protected abstract PlatformVersionInfo GetOnDiskVersionInfo();
+
+ protected abstract PlatformVersionInfo GetSdkStorageVersionInfo();
+
+ protected abstract PlatformVersionInfo GetExternalVersionInfo();
+
+ protected abstract PlatformVersionInfo GetExternalAcrVersionInfo();
+
+ protected abstract PlatformVersionInfo GetAcrVersionInfo();
+
+ private static bool HasSupportedVersions(PlatformVersionInfo versionInfo)
+ {
+ return versionInfo?.SupportedVersions != null && versionInfo.SupportedVersions.Any();
+ }
+
+ ///
+ /// Resolves version info using the 4-tier provider chain.
+ /// Priority: External ACR → External SDK → Direct ACR → CDN.
+ ///
+ private PlatformVersionInfo ResolveDynamicVersionInfo()
+ {
+ if (this.options.EnableExternalAcrSdkProvider)
+ {
+ var result = this.TryGetVersionInfo(this.GetExternalAcrVersionInfo, "external ACR");
+ if (HasSupportedVersions(result))
+ {
+ this.outputWriter.WriteLine("Version resolved using external ACR SDK provider.");
+ return result;
+ }
+ }
+
+ if (this.options.EnableExternalSdkProvider)
+ {
+ var result = this.TryGetVersionInfo(this.GetExternalVersionInfo, "external SDK");
+ if (HasSupportedVersions(result))
+ {
+ this.outputWriter.WriteLine("Version resolved using external SDK provider.");
+ return result;
+ }
+ }
+
+ if (this.options.EnableAcrSdkProvider)
+ {
+ var result = this.TryGetVersionInfo(this.GetAcrVersionInfo, "direct ACR");
+ if (HasSupportedVersions(result))
+ {
+ this.outputWriter.WriteLine("Version resolved using direct ACR SDK provider.");
+ return result;
+ }
+ }
+
+ this.outputWriter.WriteLine("Version resolved using blob SDK storage provider(CDN).");
+ return this.GetSdkStorageVersionInfo();
+ }
+
+ private PlatformVersionInfo TryGetVersionInfo(
+ Func getVersionInfo,
+ string providerName)
+ {
+ try
+ {
+ var result = getVersionInfo();
+ if (result == null)
+ {
+ this.logger.LogWarning(
+ "{ProviderName} version provider returned no version info for {PlatformName}. Trying next provider.",
+ providerName,
+ this.PlatformName);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(
+ "Error while getting version info from {ProviderName} provider for {PlatformName}. Ex: {Ex}",
+ providerName,
+ this.PlatformName,
+ ex);
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/PlatformVersionSourceType.cs b/src/BuildScriptGenerator/PlatformVersionSourceType.cs
index 8137a275b9..65ac237b04 100644
--- a/src/BuildScriptGenerator/PlatformVersionSourceType.cs
+++ b/src/BuildScriptGenerator/PlatformVersionSourceType.cs
@@ -10,5 +10,6 @@ public enum PlatformVersionSourceType
OnDisk,
AvailableOnWeb,
AvailableViaExternalProvider,
+ AvailableOnAcr,
}
}
diff --git a/src/BuildScriptGenerator/Python/PythonConstants.cs b/src/BuildScriptGenerator/Python/PythonConstants.cs
index 0a336ea011..6e319d5fe4 100644
--- a/src/BuildScriptGenerator/Python/PythonConstants.cs
+++ b/src/BuildScriptGenerator/Python/PythonConstants.cs
@@ -3,6 +3,8 @@
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
+using System.Collections.Generic;
+
namespace Microsoft.Oryx.BuildScriptGenerator.Python
{
public static class PythonConstants
@@ -20,5 +22,18 @@ public static class PythonConstants
public const string DefaultTargetPackageDirectory = "__oryx_packages__";
public const string SetupDotPyFileName = "setup.py";
public const string CondaExecutablePath = "/opt/conda/condabin/conda";
+
+ ///
+ /// Default Python major.minor version per OS flavor, matching platforms/python/versions/*/defaultVersion.txt.
+ ///
+ public static readonly Dictionary DefaultVersionPerFlavor = new Dictionary
+ {
+ { "bookworm", "3.8" },
+ { "bullseye", "3.8" },
+ { "buster", "3.8" },
+ { "focal-scm", "3.8" },
+ { "noble", "3.14" },
+ { "stretch", "3.8" },
+ };
}
}
diff --git a/src/BuildScriptGenerator/Python/PythonPlatform.cs b/src/BuildScriptGenerator/Python/PythonPlatform.cs
index 071b9fe8cd..918f997a0e 100644
--- a/src/BuildScriptGenerator/Python/PythonPlatform.cs
+++ b/src/BuildScriptGenerator/Python/PythonPlatform.cs
@@ -89,7 +89,10 @@ internal class PythonPlatform : IProgrammingPlatform
private readonly IPythonPlatformDetector detector;
private readonly PythonPlatformInstaller platformInstaller;
private readonly IExternalSdkProvider externalSdkProvider;
+ private readonly IExternalAcrSdkProvider externalAcrSdkProvider;
+ private readonly IAcrSdkProvider acrSdkProvider;
private readonly TelemetryClient telemetryClient;
+ private readonly IStandardOutputWriter outputWriter;
///
/// Initializes a new instance of the class.
@@ -108,7 +111,10 @@ public PythonPlatform(
IPythonPlatformDetector detector,
PythonPlatformInstaller platformInstaller,
IExternalSdkProvider externalSdkProvider,
- TelemetryClient telemetryClient)
+ IExternalAcrSdkProvider externalAcrSdkProvider,
+ IAcrSdkProvider acrSdkProvider,
+ TelemetryClient telemetryClient,
+ IStandardOutputWriter outputWriter)
{
this.commonOptions = commonOptions.Value;
this.pythonScriptGeneratorOptions = pythonScriptGeneratorOptions.Value;
@@ -117,7 +123,10 @@ public PythonPlatform(
this.detector = detector;
this.platformInstaller = platformInstaller;
this.externalSdkProvider = externalSdkProvider;
+ this.externalAcrSdkProvider = externalAcrSdkProvider;
+ this.acrSdkProvider = acrSdkProvider;
this.telemetryClient = telemetryClient;
+ this.outputWriter = outputWriter;
}
///
@@ -382,57 +391,63 @@ public string GetInstallerScriptSnippet(
return null;
}
- string installationScriptSnippet = null;
- if (this.commonOptions.EnableDynamicInstall)
+ if (!this.commonOptions.EnableDynamicInstall)
{
- this.logger.LogDebug("Dynamic install is enabled.");
+ this.logger.LogDebug("Dynamic install not enabled.");
+ return null;
+ }
+
+ this.logger.LogDebug("Dynamic install is enabled.");
+
+ if (this.platformInstaller.IsVersionAlreadyInstalled(detectorResult.PlatformVersion))
+ {
+ this.logger.LogDebug(
+ "Python version {version} is already installed. So skipping installing it again.",
+ detectorResult.PlatformVersion);
+ return null;
+ }
+
+ var version = detectorResult.PlatformVersion;
- if (this.platformInstaller.IsVersionAlreadyInstalled(detectorResult.PlatformVersion))
+ // Priority: External-ACR → External-SDK → Direct-ACR → CDN
+
+ // 1. Try External-ACR (socket → ACR)
+ if (this.commonOptions.EnableExternalAcrSdkProvider)
+ {
+ var result = this.TryInstallFromExternalAcrSdkProvider(version);
+
+ if (result != null)
{
- this.logger.LogDebug(
- "Python version {version} is already installed. So skipping installing it again.",
- detectorResult.PlatformVersion);
+ return result;
}
- else
+ }
+
+ // 2. Try External-SDK (socket → SDK)
+ if (this.commonOptions.EnableExternalSdkProvider)
+ {
+ var result = this.TryInstallFromExternalSdkProvider(version);
+ if (result != null)
{
- if (this.commonOptions.EnableExternalSdkProvider)
- {
- this.logger.LogDebug("Python version {version} is not installed. External SDK provider is enabled so trying to fetch SDK using it.", detectorResult.PlatformVersion);
-
- try
- {
- var blobName = BlobNameHelper.GetBlobNameForVersion(this.Name, detectorResult.PlatformVersion, this.commonOptions.DebianFlavor);
- var isExternalFetchSuccess = this.externalSdkProvider.RequestBlobAsync(this.Name, blobName).Result;
- if (isExternalFetchSuccess)
- {
- this.logger.LogDebug("Python version {version} is fetched successfully using external SDK provider. So generating an installation script snippet which skips platform binary download.", detectorResult.PlatformVersion);
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(detectorResult.PlatformVersion, skipSdkBinaryDownload: true);
- }
- else
- {
- this.logger.LogDebug("Python version {version} is not fetched successfully using external SDK provider. So generating an installation script snippet for it.", detectorResult.PlatformVersion);
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(detectorResult.PlatformVersion);
- }
- }
- catch (Exception ex)
- {
- this.logger.LogError(ex, "Error while fetching python version {version} using external SDK provider.", detectorResult.PlatformVersion);
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(detectorResult.PlatformVersion);
- }
- }
- else
- {
- this.logger.LogDebug("Python version {version} is not installed. So generating an installation script snippet for it.", detectorResult.PlatformVersion);
- installationScriptSnippet = this.platformInstaller.GetInstallerScriptSnippet(detectorResult.PlatformVersion);
- }
+ return result;
}
}
- else
+
+ // 3. Try Direct-ACR (direct OCI API calls)
+ if (this.commonOptions.EnableAcrSdkProvider)
{
- this.logger.LogDebug("Dynamic install not enabled.");
+ var result = this.TryInstallFromAcrSdkProvider(version);
+ if (result != null)
+ {
+ return result;
+ }
}
- return installationScriptSnippet;
+ // 4. CDN fallback
+ this.outputWriter.WriteLine($"Falling back to CDN for '{this.Name}' version '{version}'.");
+ this.logger.LogDebug(
+ "Python version {version} is not installed. So generating an installation script snippet for it.",
+ version);
+ return this.platformInstaller.GetInstallerScriptSnippet(version);
}
///
@@ -565,6 +580,112 @@ private static bool IsCondaInstalledInImage()
return File.Exists(PythonConstants.CondaExecutablePath);
}
+ private string TryInstallFromExternalSdkProvider(string version)
+ {
+ this.logger.LogDebug(
+ "Python version {version} is not installed. External SDK provider is enabled so trying to fetch SDK using it.",
+ version);
+
+ try
+ {
+ var blobName = BlobNameHelper.GetBlobNameForVersion(this.Name, version, this.commonOptions.DebianFlavor);
+ if (this.externalSdkProvider.RequestBlobAsync(this.Name, blobName).Result)
+ {
+ this.logger.LogDebug(
+ "Python version {version} is fetched successfully using external SDK provider. Skipping platform binary download.",
+ version);
+ this.outputWriter.WriteLine($"SDK for '{this.Name}' version '{version}' fetched via external SDK provider.");
+ return this.platformInstaller.GetInstallerScriptSnippet(version, skipSdkBinaryDownload: true);
+ }
+
+ this.logger.LogDebug(
+ "Python version {version} is not fetched successfully using external SDK provider. Generating installation script snippet.",
+ version);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via external SDK provider for '{this.Name}' version '{version}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(
+ ex,
+ "Error while fetching python version {version} using external SDK provider.",
+ version);
+ this.outputWriter.WriteLine($"Error fetching SDK via external SDK provider for '{this.Name}' version '{version}'. Trying next provider.");
+ }
+
+ return null;
+ }
+
+ private string TryInstallFromAcrSdkProvider(string version)
+ {
+ this.logger.LogDebug(
+ "Python version {version} is not installed. ACR SDK provider is enabled, so trying to fetch SDK using it.",
+ version);
+
+ try
+ {
+ var result = this.acrSdkProvider.RequestSdkFromAcrAsync(
+ this.Name, version, this.commonOptions.DebianFlavor).Result;
+
+ if (result)
+ {
+ this.logger.LogDebug(
+ "Python version {version} is fetched successfully using ACR SDK provider.",
+ version);
+ this.outputWriter.WriteLine($"SDK for '{this.Name}' version '{version}' fetched via direct ACR provider.");
+ return this.platformInstaller.GetInstallerScriptSnippet(version, skipSdkBinaryDownload: true);
+ }
+
+ this.logger.LogDebug(
+ "Python version {version} is not fetched via ACR SDK provider. Trying next provider.",
+ version);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via direct ACR provider for '{this.Name}' version '{version}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(
+ ex,
+ "Error while fetching python version {version} using ACR SDK provider. Trying next provider.",
+ version);
+ this.outputWriter.WriteLine($"Error fetching SDK via direct ACR provider for '{this.Name}' version '{version}'. Trying next provider.");
+ }
+
+ return null;
+ }
+
+ private string TryInstallFromExternalAcrSdkProvider(string version)
+ {
+ this.logger.LogDebug(
+ "Python version {version} is not installed. External ACR SDK provider is enabled, so trying to fetch SDK using it.",
+ version);
+
+ try
+ {
+ if (this.externalAcrSdkProvider.RequestSdkAsync(this.Name, version, this.commonOptions.DebianFlavor).Result)
+ {
+ this.logger.LogDebug(
+ "Python version {version} is fetched successfully using external ACR SDK provider. Skipping platform binary download.",
+ version);
+ this.outputWriter.WriteLine($"SDK for '{this.Name}' version '{version}' fetched via external ACR provider.");
+ return this.platformInstaller.GetInstallerScriptSnippet(version, skipSdkBinaryDownload: true);
+ }
+
+ this.logger.LogDebug(
+ "Python version {version} is not fetched via external ACR SDK provider. Trying next provider.",
+ version);
+ this.outputWriter.WriteLine($"Failed to fetch SDK via external ACR provider for '{this.Name}' version '{version}'. Trying next provider.");
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(
+ ex,
+ "Error while fetching python version {version} using external ACR SDK provider. Trying next provider.",
+ version);
+ this.outputWriter.WriteLine($"Error fetching SDK via external ACR provider for '{this.Name}' version '{version}'. Trying next provider.");
+ }
+
+ return null;
+ }
+
private BuildScriptSnippet GetBuildScriptSnippetForConda(
BuildScriptGeneratorContext context,
PythonPlatformDetectorResult detectorResult)
@@ -711,6 +832,17 @@ private string GetMaxSatisfyingVersionAndVerify(string version)
private string GetVersionUsingHierarchicalRules(string detectedVersion)
{
+ // If External ACR SDK provider is enabled, then we try to get the version from it first
+ // before applying the hierarchical rules, because it has the highest priority in terms of version selection.
+ if (this.commonOptions.EnableExternalAcrSdkProvider)
+ {
+ var acrVersionInfo = this.versionProvider.GetVersionInfo();
+ if (acrVersionInfo?.DefaultVersion != null)
+ {
+ return acrVersionInfo.DefaultVersion;
+ }
+ }
+
// Explicitly specified version by user wins over detected version
if (!string.IsNullOrEmpty(this.pythonScriptGeneratorOptions.PythonVersion))
{
diff --git a/src/BuildScriptGenerator/Python/PythonScriptGeneratorServiceCollectionExtensions.cs b/src/BuildScriptGenerator/Python/PythonScriptGeneratorServiceCollectionExtensions.cs
index b767f48663..59ed405da7 100644
--- a/src/BuildScriptGenerator/Python/PythonScriptGeneratorServiceCollectionExtensions.cs
+++ b/src/BuildScriptGenerator/Python/PythonScriptGeneratorServiceCollectionExtensions.cs
@@ -20,6 +20,8 @@ public static IServiceCollection AddPythonScriptGeneratorServices(this IServiceC
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
return services;
}
}
diff --git a/src/BuildScriptGenerator/Python/VersionProviders/PythonAcrVersionProvider.cs b/src/BuildScriptGenerator/Python/VersionProviders/PythonAcrVersionProvider.cs
new file mode 100644
index 0000000000..0c1ee9f174
--- /dev/null
+++ b/src/BuildScriptGenerator/Python/VersionProviders/PythonAcrVersionProvider.cs
@@ -0,0 +1,36 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System.Net.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Python
+{
+ ///
+ /// ACR-based version provider for Python SDKs.
+ /// Parallel to but uses OCI Distribution API.
+ ///
+ internal class PythonAcrVersionProvider : AcrVersionProviderBase, IPythonVersionProvider
+ {
+ private PlatformVersionInfo platformVersionInfo;
+
+ public PythonAcrVersionProvider(
+ IOptions commonOptions,
+ OciRegistryClient ociClient,
+ ILoggerFactory loggerFactory)
+ : base(commonOptions, ociClient, loggerFactory)
+ {
+ }
+
+ public virtual PlatformVersionInfo GetVersionInfo()
+ {
+ return this.platformVersionInfo
+ ??= this.GetAvailableVersionsFromAcr(
+ platformName: ToolNameConstants.PythonName,
+ defaultVersionPerFlavor: Python.PythonConstants.DefaultVersionPerFlavor);
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/Python/VersionProviders/PythonExternalAcrVersionProvider.cs b/src/BuildScriptGenerator/Python/VersionProviders/PythonExternalAcrVersionProvider.cs
new file mode 100644
index 0000000000..d3427ac584
--- /dev/null
+++ b/src/BuildScriptGenerator/Python/VersionProviders/PythonExternalAcrVersionProvider.cs
@@ -0,0 +1,40 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Python
+{
+ ///
+ /// ACR-based version provider for Python SDKs via external socket provider.
+ /// Parallel to (blob) and
+ /// (direct OCI).
+ ///
+ internal class PythonExternalAcrVersionProvider : ExternalAcrVersionProviderBase, IPythonVersionProvider
+ {
+ public PythonExternalAcrVersionProvider(
+ IOptions options,
+ ILoggerFactory loggerFactory,
+ IStandardOutputWriter outputWriter)
+ : base(options, loggerFactory, outputWriter)
+ {
+ }
+
+ public virtual PlatformVersionInfo GetVersionInfo()
+ {
+ var version = this.GetCompanionSdkVersion(platformName: ToolNameConstants.PythonName, debianFlavor: this.DebianFlavor);
+ if (string.IsNullOrEmpty(version))
+ {
+ return null;
+ }
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(
+ supportedVersions: new[] { version },
+ defaultVersion: version);
+ }
+ }
+}
diff --git a/src/BuildScriptGenerator/Python/VersionProviders/PythonVersionProvider.cs b/src/BuildScriptGenerator/Python/VersionProviders/PythonVersionProvider.cs
index b6a268e86e..a2792b4341 100644
--- a/src/BuildScriptGenerator/Python/VersionProviders/PythonVersionProvider.cs
+++ b/src/BuildScriptGenerator/Python/VersionProviders/PythonVersionProvider.cs
@@ -3,60 +3,47 @@
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
-using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.Oryx.BuildScriptGenerator.Python
{
- internal class PythonVersionProvider : IPythonVersionProvider
+ internal class PythonVersionProvider : PlatformVersionProviderBase, IPythonVersionProvider
{
- private readonly BuildScriptGeneratorOptions options;
private readonly PythonOnDiskVersionProvider onDiskVersionProvider;
private readonly PythonSdkStorageVersionProvider sdkStorageVersionProvider;
private readonly PythonExternalVersionProvider externalVersionProvider;
- private readonly ILogger logger;
- private PlatformVersionInfo versionInfo;
+ private readonly PythonExternalAcrVersionProvider externalAcrVersionProvider;
+ private readonly PythonAcrVersionProvider acrVersionProvider;
public PythonVersionProvider(
IOptions options,
PythonOnDiskVersionProvider onDiskVersionProvider,
PythonSdkStorageVersionProvider sdkStorageVersionProvider,
PythonExternalVersionProvider externalVersionProvider,
- ILogger logger)
+ PythonExternalAcrVersionProvider externalAcrVersionProvider,
+ PythonAcrVersionProvider acrVersionProvider,
+ ILogger logger,
+ IStandardOutputWriter outputWriter)
+ : base(options.Value, logger, outputWriter)
{
- this.options = options.Value;
this.onDiskVersionProvider = onDiskVersionProvider;
this.sdkStorageVersionProvider = sdkStorageVersionProvider;
this.externalVersionProvider = externalVersionProvider;
- this.logger = logger;
+ this.externalAcrVersionProvider = externalAcrVersionProvider;
+ this.acrVersionProvider = acrVersionProvider;
}
- public PlatformVersionInfo GetVersionInfo()
- {
- if (this.versionInfo == null)
- {
- if (this.options.EnableDynamicInstall)
- {
- if (this.options.EnableExternalSdkProvider)
- {
- try
- {
- return this.externalVersionProvider.GetVersionInfo();
- }
- catch (Exception ex)
- {
- this.logger.LogError($"Failed to get version info from external SDK provider. Falling back to http based sdkStorageVersionProvider. Ex: {ex}");
- }
- }
-
- return this.sdkStorageVersionProvider.GetVersionInfo();
- }
-
- this.versionInfo = this.onDiskVersionProvider.GetVersionInfo();
- }
-
- return this.versionInfo;
- }
+ protected override string PlatformName => "python";
+
+ protected override PlatformVersionInfo GetOnDiskVersionInfo() => this.onDiskVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetSdkStorageVersionInfo() => this.sdkStorageVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetExternalVersionInfo() => this.externalVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetExternalAcrVersionInfo() => this.externalAcrVersionProvider.GetVersionInfo();
+
+ protected override PlatformVersionInfo GetAcrVersionInfo() => this.acrVersionProvider.GetVersionInfo();
}
}
\ No newline at end of file
diff --git a/src/BuildScriptGenerator/SdkStorageVersionProviderBase.cs b/src/BuildScriptGenerator/SdkStorageVersionProviderBase.cs
index 4c43704974..ef3ace3913 100644
--- a/src/BuildScriptGenerator/SdkStorageVersionProviderBase.cs
+++ b/src/BuildScriptGenerator/SdkStorageVersionProviderBase.cs
@@ -66,6 +66,13 @@ protected PlatformVersionInfo GetAvailableVersionsFromStorage(string platformNam
{
xdoc = ListBlobsHelper.GetAllBlobs(sdkStorageBackupBaseUrl, platformName, httpClient);
}
+ else
+ {
+ throw new InvalidOperationException(
+ $"Failed to get SDK versions for platform '{platformName}' from primary storage URL '{sdkStorageBaseUrl}', " +
+ $"and backup storage URL is not configured. {Constants.NetworkConfigurationHelpText}",
+ ex);
+ }
}
var supportedVersions = new List();
@@ -154,7 +161,7 @@ protected string GetDefaultVersion(string platformName, string sdkStorageBaseUrl
while ((line = stringReader.ReadLine()) != null)
{
// Ignore any comments in the file
- if (!line.StartsWith("#") || !line.StartsWith("//"))
+ if (!line.StartsWith("#") && !line.StartsWith("//"))
{
defaultVersion = line.Trim();
break;
diff --git a/src/BuildScriptGeneratorCli/BuildScriptGeneratorCli.csproj b/src/BuildScriptGeneratorCli/BuildScriptGeneratorCli.csproj
index 7c98980c80..0a93a56661 100644
--- a/src/BuildScriptGeneratorCli/BuildScriptGeneratorCli.csproj
+++ b/src/BuildScriptGeneratorCli/BuildScriptGeneratorCli.csproj
@@ -24,15 +24,15 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/BuildScriptGeneratorCli/Options/BuildScriptGeneratorOptionsSetup.cs b/src/BuildScriptGeneratorCli/Options/BuildScriptGeneratorOptionsSetup.cs
index f3313c7c7e..f3c27235c6 100644
--- a/src/BuildScriptGeneratorCli/Options/BuildScriptGeneratorOptionsSetup.cs
+++ b/src/BuildScriptGeneratorCli/Options/BuildScriptGeneratorOptionsSetup.cs
@@ -62,6 +62,14 @@ public void Configure(BuildScriptGeneratorLib.BuildScriptGeneratorOptions option
// Dynamic install
options.EnableDynamicInstall = this.GetBooleanValue(SettingsKeys.EnableDynamicInstall);
options.EnableExternalSdkProvider = this.GetBooleanValue(SettingsKeys.EnableExternalSdkProvider);
+ options.EnableExternalAcrSdkProvider = this.GetBooleanValue(SettingsKeys.EnableExternalAcrSdkProvider);
+
+ // If multi-platform build is enabled, we disable the external ACR provider.
+ options.EnableExternalAcrSdkProvider = options.EnableExternalAcrSdkProvider && !options.EnableMultiPlatformBuild;
+
+ options.EnableAcrSdkProvider = this.GetBooleanValue(SettingsKeys.EnableAcrSdkProvider);
+ options.OryxAcrSdkRegistryUrl = this.GetStringValue(SettingsKeys.OryxAcrSdkRegistryUrl);
+ options.OryxAcrSdkRepositoryPrefix = this.GetStringValue(SettingsKeys.OryxAcrSdkRepositoryPrefix);
var dynamicInstallRootDir = this.GetStringValue(SettingsKeys.DynamicInstallRootDir);
diff --git a/src/BuildScriptGeneratorCli/SettingsKeys.cs b/src/BuildScriptGeneratorCli/SettingsKeys.cs
index b478745127..78aac84e08 100644
--- a/src/BuildScriptGeneratorCli/SettingsKeys.cs
+++ b/src/BuildScriptGeneratorCli/SettingsKeys.cs
@@ -17,7 +17,6 @@ public static class SettingsKeys
public const string CreatePackage = "CREATE_PACKAGE";
public const string CompressDestinationDir = "COMPRESS_DESTINATION_DIR";
public const string EnableDynamicInstall = "ENABLE_DYNAMIC_INSTALL";
- public const string EnableExternalSdkProvider = "ORYX_ENABLE_EXTERNAL_SDK_PROVIDER";
public const string DisableCheckers = "DISABLE_CHECKERS";
public const string DisableDotNetCoreBuild = "DISABLE_DOTNETCORE_BUILD";
public const string DisableGolangBuild = "DISABLE_GOLANG_BUILD";
@@ -74,6 +73,13 @@ public static class SettingsKeys
public const string OsFlavor = "OS_FLAVOR";
public const string DebianFlavor = "DEBIAN_FLAVOR";
public const string CallerId = "CALLER_ID";
+
+ // SDK provider settings
public const string OryxDisablePipUpgrade = "ORYX_DISABLE_PIP_UPGRADE";
+ public const string EnableExternalSdkProvider = "ORYX_ENABLE_EXTERNAL_SDK_PROVIDER";
+ public const string EnableExternalAcrSdkProvider = "ORYX_ENABLE_EXTERNAL_ACR_SDK_PROVIDER";
+ public const string EnableAcrSdkProvider = "ORYX_ENABLE_ACR_SDK_PROVIDER";
+ public const string OryxAcrSdkRegistryUrl = "ORYX_ACR_SDK_REGISTRY_URL";
+ public const string OryxAcrSdkRepositoryPrefix = "ORYX_ACR_SDK_REPOSITORY_PREFIX";
}
}
diff --git a/src/Oryx.Common/Common.csproj b/src/Oryx.Common/Common.csproj
index 1d28def0ad..7b53106443 100644
--- a/src/Oryx.Common/Common.csproj
+++ b/src/Oryx.Common/Common.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/tests/BuildScriptGenerator.Tests/AcrSdkProviderTest.cs b/tests/BuildScriptGenerator.Tests/AcrSdkProviderTest.cs
new file mode 100644
index 0000000000..7e9cf7c2a8
--- /dev/null
+++ b/tests/BuildScriptGenerator.Tests/AcrSdkProviderTest.cs
@@ -0,0 +1,342 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Microsoft.Oryx.Tests.Common;
+using Xunit;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Tests
+{
+ public class AcrSdkProviderTest
+ {
+ // --- Constructor arg validation ---
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_ThrowsForNullPlatformName()
+ {
+ var provider = CreateProvider();
+ await Assert.ThrowsAsync(
+ () => provider.RequestSdkFromAcrAsync(null, "1.0", "bookworm"));
+ }
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_ThrowsForEmptyPlatformName()
+ {
+ var provider = CreateProvider();
+ await Assert.ThrowsAsync(
+ () => provider.RequestSdkFromAcrAsync(string.Empty, "1.0", "bookworm"));
+ }
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_ThrowsForNullVersion()
+ {
+ var provider = CreateProvider();
+ await Assert.ThrowsAsync(
+ () => provider.RequestSdkFromAcrAsync("nodejs", null, "bookworm"));
+ }
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_ThrowsForEmptyVersion()
+ {
+ var provider = CreateProvider();
+ await Assert.ThrowsAsync(
+ () => provider.RequestSdkFromAcrAsync("nodejs", string.Empty, "bookworm"));
+ }
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_DefaultsDebianFlavor_WhenEmpty()
+ {
+ // Should NOT throw — debianFlavor defaults to "bookworm" when empty
+ var handler = new FailingHandler();
+ var provider = CreateProvider(handler);
+
+ // Will fail at the HTTP level, but should not throw ArgumentException for empty debianFlavor
+ var result = await provider.RequestSdkFromAcrAsync("nodejs", "20.0.0", string.Empty);
+ Assert.False(result);
+ }
+
+ // --- Tag construction ---
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_ConstructsSimpleTag_WhenNoRuntimeVersion()
+ {
+ // When runtimeVersion is null, tag should be "{flavor}-{version}"
+ var handler = new TagCapturingHandler();
+ var provider = CreateProvider(handler);
+
+ await provider.RequestSdkFromAcrAsync("nodejs", "20.19.3", "bookworm");
+
+ // The handler captures the manifest URL to verify tag construction
+ Assert.Contains("bookworm-20.19.3", handler.LastManifestUrl);
+ Assert.DoesNotContain("_", handler.LastManifestUrl);
+ }
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_ConstructsCompoundTag_WithRuntimeVersion()
+ {
+ // When runtimeVersion is provided, tag should be "{flavor}-{version}_{runtimeVersion}"
+ var handler = new TagCapturingHandler();
+ var provider = CreateProvider(handler);
+
+ await provider.RequestSdkFromAcrAsync("dotnet", "8.0.403", "bookworm", "8.0.18");
+
+ Assert.Contains("bookworm-8.0.403_8.0.18", handler.LastManifestUrl);
+ }
+
+ // --- Error handling ---
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_ReturnsFalse_WhenManifestNotFound()
+ {
+ var handler = new FailingHandler();
+ var provider = CreateProvider(handler);
+
+ var result = await provider.RequestSdkFromAcrAsync("nodejs", "20.0.0", "bookworm");
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_ReturnsFalse_WhenManifestHasNoLayers()
+ {
+ // Arrange — HEAD returns digest OK, GET manifest returns manifest with no layers
+ var handler = new ManifestHandler(layerDigest: null);
+ var provider = CreateProvider(handler);
+
+ // Act
+ var result = await provider.RequestSdkFromAcrAsync("nodejs", "20.0.0", "bookworm");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_ReturnsFalse_WhenHttpThrows()
+ {
+ // Arrange — handler that throws on all requests
+ var handler = new ThrowingHandler();
+ var provider = CreateProvider(handler);
+
+ // Act
+ var result = await provider.RequestSdkFromAcrAsync("nodejs", "20.0.0", "bookworm");
+
+ // Assert — exceptions are caught and return false
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_ReturnsFalse_WhenServerError()
+ {
+ // Arrange — 500 Internal Server Error
+ var handler = new StatusCodeHandler(HttpStatusCode.InternalServerError);
+ var provider = CreateProvider(handler);
+
+ // Act
+ var result = await provider.RequestSdkFromAcrAsync("nodejs", "20.0.0", "bookworm");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task RequestSdkFromAcrAsync_UsesDefaultRepositoryPrefix()
+ {
+ // Arrange — verify the repository path includes the default "oryx" prefix
+ var handler = new TagCapturingHandler();
+ var provider = CreateProvider(handler);
+
+ // Act
+ await provider.RequestSdkFromAcrAsync("python", "3.11.0", "bookworm");
+
+ // Assert — URL should contain "oryx/python-sdk"
+ Assert.Contains("oryx/python-sdk", handler.LastManifestUrl);
+ }
+
+ // --- ExternalAcrSdkProvider arg validation ---
+
+ [Fact]
+ public async Task ExternalAcrSdkProvider_RequestSdkAsync_ThrowsForNullPlatformName()
+ {
+ var provider = new ExternalAcrSdkProvider(
+ new DefaultStandardOutputWriter(),
+ NullLogger.Instance);
+
+ await Assert.ThrowsAsync(
+ () => provider.RequestSdkAsync(null, "1.0", "bookworm"));
+ }
+
+ [Fact]
+ public async Task ExternalAcrSdkProvider_RequestSdkAsync_ThrowsForNullVersion()
+ {
+ var provider = new ExternalAcrSdkProvider(
+ new DefaultStandardOutputWriter(),
+ NullLogger.Instance);
+
+ await Assert.ThrowsAsync(
+ () => provider.RequestSdkAsync("nodejs", null, "bookworm"));
+ }
+
+ [Fact]
+ public async Task ExternalAcrSdkProvider_RequestSdkAsync_ThrowsForNullDebianFlavor()
+ {
+ var provider = new ExternalAcrSdkProvider(
+ new DefaultStandardOutputWriter(),
+ NullLogger.Instance);
+
+ await Assert.ThrowsAsync(
+ () => provider.RequestSdkAsync("nodejs", "1.0", null));
+ }
+
+ // --- Helpers ---
+
+ private static AcrSdkProvider CreateProvider(HttpMessageHandler handler = null)
+ {
+ var options = Options.Create(new BuildScriptGeneratorOptions
+ {
+ OryxAcrSdkRegistryUrl = "https://test.azurecr.io",
+ DynamicInstallRootDir = "/tmp/oryx-test",
+ DebianFlavor = "bookworm",
+ });
+
+ var httpHandler = handler ?? new FailingHandler();
+ var factory = new StubHttpClientFactory(httpHandler);
+
+ var ociClient = new OciRegistryClient("https://test.azurecr.io", factory, NullLoggerFactory.Instance);
+ return new AcrSdkProvider(
+ new DefaultStandardOutputWriter(),
+ NullLogger.Instance,
+ options,
+ ociClient);
+ }
+
+ private class StubHttpClientFactory : IHttpClientFactory
+ {
+ private readonly HttpMessageHandler _handler;
+
+ public StubHttpClientFactory(HttpMessageHandler handler)
+ {
+ _handler = handler;
+ }
+
+ public HttpClient CreateClient(string name)
+ {
+ return new HttpClient(_handler);
+ }
+ }
+
+ ///
+ /// Handler that returns 404 for all requests, simulating missing images.
+ ///
+ private class FailingHandler : HttpMessageHandler
+ {
+ protected override Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
+ }
+ }
+
+ ///
+ /// Handler that captures the manifest URL and returns a 404 for the HEAD request.
+ /// Used to verify tag construction without needing full OCI flow.
+ ///
+ private class TagCapturingHandler : HttpMessageHandler
+ {
+ public string LastManifestUrl { get; private set; }
+
+ protected override Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var url = request.RequestUri.ToString();
+
+ if (url.Contains("/manifests/"))
+ {
+ LastManifestUrl = url;
+ }
+
+ // Return 404 to short-circuit — we only need to verify the URL was constructed correctly
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
+ }
+ }
+
+ ///
+ /// Handler that throws on all requests, simulating network failures.
+ ///
+ private class ThrowingHandler : HttpMessageHandler
+ {
+ protected override Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ throw new HttpRequestException("Simulated network failure");
+ }
+ }
+
+ ///
+ /// Handler that returns a specific status code for all requests.
+ ///
+ private class StatusCodeHandler : HttpMessageHandler
+ {
+ private readonly HttpStatusCode _statusCode;
+
+ public StatusCodeHandler(HttpStatusCode statusCode)
+ {
+ _statusCode = statusCode;
+ }
+
+ protected override Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new HttpResponseMessage(_statusCode));
+ }
+ }
+
+ ///
+ /// Handler that simulates a manifest with optional layer digest.
+ /// Returns digest on HEAD, manifest on GET, 404 on blob download.
+ ///
+ private class ManifestHandler : HttpMessageHandler
+ {
+ private readonly string _layerDigest;
+
+ public ManifestHandler(string layerDigest)
+ {
+ _layerDigest = layerDigest;
+ }
+
+ protected override Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var url = request.RequestUri.ToString();
+
+ if (url.Contains("/manifests/") && request.Method == HttpMethod.Head)
+ {
+ var resp = new HttpResponseMessage(HttpStatusCode.OK);
+ resp.Headers.Add("Docker-Content-Digest", "sha256:testdigest");
+ return Task.FromResult(resp);
+ }
+
+ if (url.Contains("/manifests/") && request.Method == HttpMethod.Get)
+ {
+ var manifest = _layerDigest != null
+ ? $"{{\"schemaVersion\":2,\"layers\":[{{\"digest\":\"{_layerDigest}\",\"size\":100}}]}}"
+ : "{\"schemaVersion\":2,\"layers\":[]}";
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(manifest),
+ });
+ }
+
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
+ }
+ }
+ }
+}
diff --git a/tests/BuildScriptGenerator.Tests/BuildScriptGenerator.Tests.csproj b/tests/BuildScriptGenerator.Tests/BuildScriptGenerator.Tests.csproj
index 8a5f4bd900..5df219d0e5 100644
--- a/tests/BuildScriptGenerator.Tests/BuildScriptGenerator.Tests.csproj
+++ b/tests/BuildScriptGenerator.Tests/BuildScriptGenerator.Tests.csproj
@@ -16,8 +16,8 @@
-
-
+
+
diff --git a/tests/BuildScriptGenerator.Tests/DefaultBuildScriptGeneratorTest.cs b/tests/BuildScriptGenerator.Tests/DefaultBuildScriptGeneratorTest.cs
index 1482d4f734..57f7e7d956 100644
--- a/tests/BuildScriptGenerator.Tests/DefaultBuildScriptGeneratorTest.cs
+++ b/tests/BuildScriptGenerator.Tests/DefaultBuildScriptGeneratorTest.cs
@@ -620,6 +620,7 @@ private DefaultBuildScriptGenerator CreateDefaultScriptGenerator(
var defaultPlatformDetector = new DefaultPlatformsInformationProvider(
platforms,
new DefaultStandardOutputWriter(),
+ NullLogger.Instance,
Options.Create(commonOptions));
var envScriptProvider = new BuildScriptGenerator.PlatformsInstallationScriptProvider(
platforms,
diff --git a/tests/BuildScriptGenerator.Tests/DefaultPlatformDetectorTest.cs b/tests/BuildScriptGenerator.Tests/DefaultPlatformDetectorTest.cs
index 48112676c2..e5b26ee0b0 100644
--- a/tests/BuildScriptGenerator.Tests/DefaultPlatformDetectorTest.cs
+++ b/tests/BuildScriptGenerator.Tests/DefaultPlatformDetectorTest.cs
@@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.Linq;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Oryx.Detector;
using Moq;
@@ -116,6 +118,7 @@ private DefaultPlatformsInformationProvider CreatePlatformDetector(
return new DefaultPlatformsInformationProvider(
platforms,
new DefaultStandardOutputWriter(),
+ NullLogger.Instance,
Options.Create(new BuildScriptGeneratorOptions()));
}
diff --git a/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCoreAcrVersionProviderTest.cs b/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCoreAcrVersionProviderTest.cs
new file mode 100644
index 0000000000..c0703815b3
--- /dev/null
+++ b/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCoreAcrVersionProviderTest.cs
@@ -0,0 +1,201 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator.DotNetCore;
+using Microsoft.Oryx.Tests.Common;
+using Xunit;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Tests.DotnetCore
+{
+ public class DotNetCoreAcrVersionProviderTest
+ {
+ // --- Tag parsing ---
+
+ [Fact]
+ public void GetSupportedVersions_ParsesCompoundTags_IntoVersionMap()
+ {
+ // Arrange: tags in format {os}-{sdkVersion}_{runtimeVersion}
+ var tags = new List
+ {
+ "bookworm-8.0.403_8.0.18",
+ "bookworm-9.0.100_9.0.0",
+ };
+
+ var provider = CreateProvider(tags, debianFlavor: "bookworm");
+
+ // Act
+ var versions = provider.GetSupportedVersions();
+
+ // Assert
+ Assert.NotNull(versions);
+ Assert.Equal(2, versions.Count);
+ Assert.Equal("8.0.403", versions["8.0.18"]);
+ Assert.Equal("9.0.100", versions["9.0.0"]);
+ }
+
+ [Fact]
+ public void GetSupportedVersions_SkipsTagsWithWrongFlavor()
+ {
+ var tags = new List
+ {
+ "bookworm-8.0.403_8.0.18",
+ "noble-9.0.100_9.0.0",
+ };
+
+ var provider = CreateProvider(tags, debianFlavor: "bookworm");
+ var versions = provider.GetSupportedVersions();
+
+ Assert.NotNull(versions);
+ Assert.Single(versions);
+ Assert.Equal("8.0.403", versions["8.0.18"]);
+ }
+
+ [Fact]
+ public void GetSupportedVersions_SkipsMalformedTags_MissingUnderscore()
+ {
+ var tags = new List
+ {
+ "bookworm-8.0.403_8.0.18",
+ "bookworm-malformed-no-underscore",
+ };
+
+ var provider = CreateProvider(tags, debianFlavor: "bookworm");
+ var versions = provider.GetSupportedVersions();
+
+ Assert.NotNull(versions);
+ Assert.Single(versions);
+ }
+
+ [Fact]
+ public void GetSupportedVersions_ReturnsNull_WhenNoMatchingTags()
+ {
+ var tags = new List
+ {
+ "noble-8.0.403_8.0.18",
+ };
+
+ var provider = CreateProvider(tags, debianFlavor: "bookworm");
+ var versions = provider.GetSupportedVersions();
+
+ // Returns null because the version map is empty (no tags match "bookworm" flavor)
+ Assert.Null(versions);
+ }
+
+ [Fact]
+ public void GetSupportedVersions_HandlesDuplicateRuntimeKeys()
+ {
+ // When multiple tags map to the same runtime, the last one wins (Dictionary behavior)
+ var tags = new List
+ {
+ "bookworm-8.0.301_8.0.18",
+ "bookworm-8.0.403_8.0.18",
+ };
+
+ var provider = CreateProvider(tags, debianFlavor: "bookworm");
+ var versions = provider.GetSupportedVersions();
+
+ Assert.NotNull(versions);
+ Assert.Single(versions);
+ Assert.Equal("8.0.403", versions["8.0.18"]);
+ }
+
+ [Fact]
+ public void GetDefaultRuntimeVersion_ReturnsNull_WhenNoVersionMap()
+ {
+ var provider = CreateProvider(new List(), debianFlavor: "bookworm");
+ var defaultVersion = provider.GetDefaultRuntimeVersion();
+ Assert.Null(defaultVersion);
+ }
+
+ // --- Helpers ---
+
+ private static TestDotNetCoreAcrVersionProvider CreateProvider(
+ List tags,
+ string debianFlavor)
+ {
+ var options = Options.Create(new BuildScriptGeneratorOptions
+ {
+ DebianFlavor = debianFlavor,
+ OryxAcrSdkRegistryUrl = "https://test.azurecr.io",
+ });
+
+ var handler = new StubHttpMessageHandler(tags);
+ var httpClientFactory = new StubHttpClientFactory(handler);
+ var ociClient = new OciRegistryClient("https://test.azurecr.io", httpClientFactory, NullLoggerFactory.Instance);
+
+ return new TestDotNetCoreAcrVersionProvider(options, ociClient, NullLoggerFactory.Instance);
+ }
+
+ ///
+ /// Test subclass that overrides HTTP behavior to return predefined tags.
+ ///
+ private class TestDotNetCoreAcrVersionProvider : DotNetCoreAcrVersionProvider
+ {
+ public TestDotNetCoreAcrVersionProvider(
+ IOptions options,
+ OciRegistryClient ociClient,
+ ILoggerFactory loggerFactory)
+ : base(options, ociClient, loggerFactory)
+ {
+ }
+ }
+
+ private class StubHttpClientFactory : IHttpClientFactory
+ {
+ private readonly HttpMessageHandler _handler;
+
+ public StubHttpClientFactory(HttpMessageHandler handler)
+ {
+ _handler = handler;
+ }
+
+ public HttpClient CreateClient(string name)
+ {
+ return new HttpClient(_handler);
+ }
+ }
+
+ ///
+ /// Returns a tag list JSON response for any tags/list request.
+ /// Returns 404 for token endpoints (simulating MCR-style public access).
+ ///
+ private class StubHttpMessageHandler : HttpMessageHandler
+ {
+ private readonly List _tags;
+
+ public StubHttpMessageHandler(List tags)
+ {
+ _tags = tags;
+ }
+
+ protected override Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var url = request.RequestUri.ToString();
+
+ if (url.Contains("/tags/list"))
+ {
+ var tagList = new OciTagList { Name = "test", Tags = _tags };
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(tagList)),
+ });
+ }
+
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
+ }
+ }
+ }
+}
diff --git a/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCoreBuildScriptGenerationTest.cs b/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCoreBuildScriptGenerationTest.cs
index 3621abb421..f235e75793 100644
--- a/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCoreBuildScriptGenerationTest.cs
+++ b/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCoreBuildScriptGenerationTest.cs
@@ -6,6 +6,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator;
using Microsoft.Oryx.BuildScriptGenerator.DotNetCore;
using Microsoft.Oryx.Detector.DotNetCore;
using Xunit;
@@ -94,12 +95,16 @@ private DotNetCorePlatform CreateDotNetCorePlatform(
Options.Create(DotNetCoreScriptGeneratorOptions),
Options.Create(commonOptions),
versionProvider,
+ new DotNetCoreExternalAcrVersionProvider(Options.Create(new BuildScriptGeneratorOptions()), NullLoggerFactory.Instance, new DefaultStandardOutputWriter()),
NullLogger.Instance,
detector,
DotNetCoreInstaller,
globalJsonSdkResolver,
- externalSdkProvider,
- TelemetryClientHelper.GetTelemetryClient());
+ externalSdkProvider,
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
}
private class TestDotNetCorePlatform : DotNetCorePlatform
@@ -108,14 +113,19 @@ public TestDotNetCorePlatform(
IOptions DotNetCoreScriptGeneratorOptions,
IOptions commonOptions,
IDotNetCoreVersionProvider DotNetCoreVersionProvider,
+ DotNetCoreExternalAcrVersionProvider externalAcrVersionProvider,
ILogger logger,
IDotNetCorePlatformDetector detector,
DotNetCorePlatformInstaller DotNetCoreInstaller,
GlobalJsonSdkResolver globalJsonSdkResolver,
IExternalSdkProvider externalSdkProvider,
- TelemetryClient telemetryClient)
+ IExternalAcrSdkProvider externalAcrSdkProvider,
+ IAcrSdkProvider acrSdkProvider,
+ TelemetryClient telemetryClient,
+ IStandardOutputWriter outputWriter)
: base(
DotNetCoreVersionProvider,
+ externalAcrVersionProvider,
logger,
detector,
commonOptions,
@@ -123,7 +133,10 @@ public TestDotNetCorePlatform(
DotNetCoreInstaller,
globalJsonSdkResolver,
externalSdkProvider,
- telemetryClient)
+ externalAcrSdkProvider,
+ acrSdkProvider,
+ telemetryClient,
+ outputWriter)
{
}
}
diff --git a/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCorePlatformTest.cs b/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCorePlatformTest.cs
index 328819275a..83a67e281e 100644
--- a/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCorePlatformTest.cs
+++ b/tests/BuildScriptGenerator.Tests/DotnetCore/DotNetCorePlatformTest.cs
@@ -1,12 +1,15 @@
-// --------------------------------------------------------------------------------------------
+// --------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
using System.Collections.Generic;
using Microsoft.ApplicationInsights;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator;
+using Microsoft.Oryx.BuildScriptGenerator.Common;
using Microsoft.Oryx.BuildScriptGenerator.DotNetCore;
using Microsoft.Oryx.BuildScriptGenerator.Exceptions;
using Microsoft.Oryx.Detector.DotNetCore;
@@ -119,6 +122,304 @@ public void Detect_ReturnsExpectedVersion_BasedOnHierarchy(
Assert.Equal(expectedSdkVersion, result.PlatformVersion);
}
+ [Fact]
+ public void GetInstallerScript_UsesExternalAcrProvider_IfEnabled_AndSdkIsNotAlreadyInstalled()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: true);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var detector = CreateDetector(detectedVersion: "3.1");
+ var platform = CreatePlatformWithProviders(
+ detector,
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider);
+ var context = CreateContext();
+ var detectorResult = new DotNetCorePlatformDetectorResult
+ {
+ Platform = DotNetCoreConstants.PlatformName,
+ PlatformVersion = "3.1.2",
+ SdkVersion = "3.1.2",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.Equal(TestDotNetCorePlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, snippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ }
+
+ [Fact]
+ public void GetInstallerScript_UsesDirectAcrProvider_IfEnabled_AndSdkIsNotAlreadyInstalled()
+ {
+ // Arrange
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var detector = CreateDetector(detectedVersion: "3.1");
+ var platform = CreatePlatformWithProviders(
+ detector,
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ acrSdkProvider: acrSdkProvider);
+ var context = CreateContext();
+ var detectorResult = new DotNetCorePlatformDetectorResult
+ {
+ Platform = DotNetCoreConstants.PlatformName,
+ PlatformVersion = "3.1.2",
+ SdkVersion = "3.1.2",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.Equal(TestDotNetCorePlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, snippet);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void GetInstallerScript_FallsBackFromExternalAcrToExternalSdk_WhenExternalAcrFails()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var detector = CreateDetector(detectedVersion: "3.1");
+ var platform = CreatePlatformWithProviders(
+ detector,
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider);
+ var context = CreateContext();
+ var detectorResult = new DotNetCorePlatformDetectorResult
+ {
+ Platform = DotNetCoreConstants.PlatformName,
+ PlatformVersion = "3.1.2",
+ SdkVersion = "3.1.2",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.Equal(TestDotNetCorePlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, snippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ }
+
+ [Fact]
+ public void GetInstallerScript_FallsBackToCdn_WhenAllProvidersFail()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: false);
+ var externalSdkProvider = new TestExternalSdkProvider(requestBlobResult: false);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var detector = CreateDetector(detectedVersion: "3.1");
+ var platform = CreatePlatformWithProviders(
+ detector,
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider,
+ acrSdkProvider: acrSdkProvider,
+ externalSdkProvider: externalSdkProvider);
+ var context = CreateContext();
+ var detectorResult = new DotNetCorePlatformDetectorResult
+ {
+ Platform = DotNetCoreConstants.PlatformName,
+ PlatformVersion = "3.1.2",
+ SdkVersion = "3.1.2",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.Equal(TestDotNetCorePlatformInstaller.InstallerScript, snippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void GetInstallerScript_DirectAcrProvider_PassesRuntimeVersion()
+ {
+ // Arrange
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var detector = CreateDetector(detectedVersion: "3.1");
+ var platform = CreatePlatformWithProviders(
+ detector,
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ acrSdkProvider: acrSdkProvider,
+ supportedVersions: new Dictionary
+ {
+ { "3.1.2", "3.1.2" },
+ });
+ var context = CreateContext();
+ var detectorResult = new DotNetCorePlatformDetectorResult
+ {
+ Platform = DotNetCoreConstants.PlatformName,
+ PlatformVersion = "3.1.2",
+ SdkVersion = "3.1.2",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ Assert.Equal(DotNetCoreConstants.PlatformName, acrSdkProvider.LastRequestedPlatformName);
+ Assert.Equal("3.1.2", acrSdkProvider.LastRequestedVersion);
+ Assert.Equal(OsTypes.DebianBookworm, acrSdkProvider.LastRequestedDebianFlavor);
+ Assert.Equal("3.1.2", acrSdkProvider.LastRequestedRuntimeVersion);
+ }
+
+ [Fact]
+ public void GetInstallerScript_FallsBackFromExternalAcrToDirectAcr_WhenBothExternalProvidersFail()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var externalSdkProvider = new TestExternalSdkProvider(requestBlobResult: false);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var detector = CreateDetector(detectedVersion: "3.1");
+ var platform = CreatePlatformWithProviders(
+ detector,
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider,
+ acrSdkProvider: acrSdkProvider,
+ externalSdkProvider: externalSdkProvider);
+ var context = CreateContext();
+ var detectorResult = new DotNetCorePlatformDetectorResult
+ {
+ Platform = DotNetCoreConstants.PlatformName,
+ PlatformVersion = "3.1.2",
+ SdkVersion = "3.1.2",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.Equal(TestDotNetCorePlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, snippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void ResolveVersions_UsesExternalAcrSdkVersion_WhenEnabled()
+ {
+ // Arrange
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var detector = CreateDetector(detectedVersion: "3.1");
+ var externalAcrVersionProvider = new TestDotNetCoreExternalAcrVersionProvider(
+ Options.Create(commonOptions), NullLoggerFactory.Instance, new DefaultStandardOutputWriter(),
+ sdkVersion: "3.1.415");
+ var platform = CreatePlatformWithExternalAcrVersionProvider(
+ detector,
+ commonOptions: commonOptions,
+ externalAcrVersionProvider: externalAcrVersionProvider,
+ supportedVersions: new Dictionary
+ {
+ { "3.1.2", "3.1.302" },
+ });
+ var context = CreateContext();
+ var detectorResult = new DotNetCorePlatformDetectorResult
+ {
+ Platform = DotNetCoreConstants.PlatformName,
+ PlatformVersion = "3.1",
+ };
+
+ // Act
+ platform.ResolveVersions(context, detectorResult);
+
+ // Assert - ExternalACR dictates SDK version, overriding normal map lookup
+ Assert.Equal("3.1.415", detectorResult.SdkVersion);
+ }
+
+ [Fact]
+ public void ResolveVersions_FallsBackToVersionMap_WhenExternalAcrReturnsNull()
+ {
+ // Arrange
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var detector = CreateDetector(detectedVersion: "3.1");
+ var externalAcrVersionProvider = new TestDotNetCoreExternalAcrVersionProvider(
+ Options.Create(commonOptions), NullLoggerFactory.Instance, new DefaultStandardOutputWriter(),
+ sdkVersion: null);
+ var platform = CreatePlatformWithExternalAcrVersionProvider(
+ detector,
+ commonOptions: commonOptions,
+ externalAcrVersionProvider: externalAcrVersionProvider,
+ supportedVersions: new Dictionary
+ {
+ { "3.1.2", "3.1.302" },
+ });
+ var context = CreateContext();
+ var detectorResult = new DotNetCorePlatformDetectorResult
+ {
+ Platform = DotNetCoreConstants.PlatformName,
+ PlatformVersion = "3.1",
+ };
+
+ // Act
+ platform.ResolveVersions(context, detectorResult);
+
+ // Assert - Falls back to normal version map
+ Assert.Equal("3.1.302", detectorResult.SdkVersion);
+ }
+
private BuildScriptGeneratorContext CreateContext(ISourceRepo sourceRepo = null)
{
sourceRepo = sourceRepo ?? new MemorySourceRepo();
@@ -160,28 +461,79 @@ private DotNetCorePlatform CreatePlatform(
var globalJsonSdkResolver = new GlobalJsonSdkResolver(NullLogger.Instance);
return new TestDotNetCorePlatform(
versionProvider,
+ new DotNetCoreExternalAcrVersionProvider(Options.Create(new BuildScriptGeneratorOptions()), NullLoggerFactory.Instance, new DefaultStandardOutputWriter()),
+ detector,
+ Options.Create(commonOptions),
+ Options.Create(dotNetCoreScriptGeneratorOptions),
+ installer,
+ globalJsonSdkResolver,
+ externalSdkProvider,
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
+ }
+
+ private DotNetCorePlatform CreatePlatformWithProviders(
+ IDotNetCorePlatformDetector detector,
+ BuildScriptGeneratorOptions commonOptions = null,
+ bool sdkAlreadyInstalled = true,
+ Dictionary supportedVersions = null,
+ IExternalAcrSdkProvider externalAcrSdkProvider = null,
+ IAcrSdkProvider acrSdkProvider = null,
+ IExternalSdkProvider externalSdkProvider = null)
+ {
+ commonOptions = commonOptions ?? new BuildScriptGeneratorOptions();
+ var defaultVersion = DotNetCoreRunTimeVersions.NetCoreApp31;
+ supportedVersions = supportedVersions ?? new Dictionary
+ {
+ { defaultVersion, defaultVersion },
+ };
+ var versionProvider = new TestDotNetCoreVersionProvider(
+ supportedVersions,
+ defaultVersion);
+ externalSdkProvider = externalSdkProvider ?? new TestExternalSdkProvider();
+ externalAcrSdkProvider = externalAcrSdkProvider ?? new TestExternalAcrSdkProvider();
+ acrSdkProvider = acrSdkProvider ?? new TestAcrSdkProvider();
+ var dotNetCoreScriptGeneratorOptions = new DotNetCoreScriptGeneratorOptions();
+ var installer = new TestDotNetCorePlatformInstaller(
+ Options.Create(commonOptions),
+ sdkAlreadyInstalled,
+ NullLoggerFactory.Instance);
+ var globalJsonSdkResolver = new GlobalJsonSdkResolver(NullLogger.Instance);
+ return new TestDotNetCorePlatform(
+ versionProvider,
+ new DotNetCoreExternalAcrVersionProvider(Options.Create(new BuildScriptGeneratorOptions()), NullLoggerFactory.Instance, new DefaultStandardOutputWriter()),
detector,
Options.Create(commonOptions),
Options.Create(dotNetCoreScriptGeneratorOptions),
installer,
globalJsonSdkResolver,
externalSdkProvider,
- TelemetryClientHelper.GetTelemetryClient());
+ externalAcrSdkProvider,
+ acrSdkProvider,
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
}
private class TestDotNetCorePlatform : DotNetCorePlatform
{
public TestDotNetCorePlatform(
IDotNetCoreVersionProvider versionProvider,
+ DotNetCoreExternalAcrVersionProvider externalAcrVersionProvider,
IDotNetCorePlatformDetector detector,
IOptions cliOptions,
IOptions dotNetCoreScriptGeneratorOptions,
DotNetCorePlatformInstaller platformInstaller,
GlobalJsonSdkResolver globalJsonSdkResolver,
IExternalSdkProvider externalSdkProvider,
- TelemetryClient telemetryClient)
+ IExternalAcrSdkProvider externalAcrSdkProvider,
+ IAcrSdkProvider acrSdkProvider,
+ TelemetryClient telemetryClient,
+ IStandardOutputWriter outputWriter)
: base(
versionProvider,
+ externalAcrVersionProvider,
NullLogger.Instance,
detector,
cliOptions,
@@ -189,9 +541,100 @@ public TestDotNetCorePlatform(
platformInstaller,
globalJsonSdkResolver,
externalSdkProvider,
- telemetryClient)
+ externalAcrSdkProvider,
+ acrSdkProvider,
+ telemetryClient,
+ outputWriter)
+ {
+ }
+ }
+
+ private class TestDotNetCorePlatformInstaller : DotNetCorePlatformInstaller
+ {
+ public static string InstallerScript = "installer-script-snippet";
+ public static string InstallerScriptWithSkipSdkBinaryDownload = "installer-script-snippet-with-skip-sdk-binary-download";
+ private readonly bool _sdkIsAlreadyInstalled;
+
+ public TestDotNetCorePlatformInstaller(
+ IOptions cliOptions,
+ bool sdkIsAlreadyInstalled,
+ ILoggerFactory loggerFactory)
+ : base(cliOptions, loggerFactory)
+ {
+ _sdkIsAlreadyInstalled = sdkIsAlreadyInstalled;
+ }
+
+ public override bool IsVersionAlreadyInstalled(string version)
+ {
+ return _sdkIsAlreadyInstalled;
+ }
+
+ public override string GetInstallerScriptSnippet(string version, bool skipSdkBinaryDownload = false)
+ {
+ if (skipSdkBinaryDownload)
+ {
+ return InstallerScriptWithSkipSdkBinaryDownload;
+ }
+
+ return InstallerScript;
+ }
+ }
+
+ private class TestDotNetCoreExternalAcrVersionProvider : DotNetCoreExternalAcrVersionProvider
+ {
+ private readonly string _sdkVersion;
+
+ public TestDotNetCoreExternalAcrVersionProvider(
+ IOptions options,
+ ILoggerFactory loggerFactory,
+ IStandardOutputWriter outputWriter,
+ string sdkVersion)
+ : base(options, loggerFactory, outputWriter)
+ {
+ _sdkVersion = sdkVersion;
+ }
+
+ public override string GetSdkVersion()
{
+ return _sdkVersion;
}
}
+
+ private DotNetCorePlatform CreatePlatformWithExternalAcrVersionProvider(
+ IDotNetCorePlatformDetector detector,
+ BuildScriptGeneratorOptions commonOptions = null,
+ TestDotNetCoreExternalAcrVersionProvider externalAcrVersionProvider = null,
+ Dictionary supportedVersions = null)
+ {
+ commonOptions = commonOptions ?? new BuildScriptGeneratorOptions();
+ var defaultVersion = DotNetCoreRunTimeVersions.NetCoreApp31;
+ supportedVersions = supportedVersions ?? new Dictionary
+ {
+ { defaultVersion, defaultVersion },
+ };
+ var versionProvider = new TestDotNetCoreVersionProvider(
+ supportedVersions,
+ defaultVersion);
+ externalAcrVersionProvider = externalAcrVersionProvider ?? new TestDotNetCoreExternalAcrVersionProvider(
+ Options.Create(commonOptions), NullLoggerFactory.Instance, new DefaultStandardOutputWriter(), sdkVersion: null);
+ var dotNetCoreScriptGeneratorOptions = new DotNetCoreScriptGeneratorOptions();
+ var installer = new DotNetCorePlatformInstaller(
+ Options.Create(commonOptions),
+ NullLoggerFactory.Instance);
+ var globalJsonSdkResolver = new GlobalJsonSdkResolver(NullLogger.Instance);
+ return new TestDotNetCorePlatform(
+ versionProvider,
+ externalAcrVersionProvider,
+ detector,
+ Options.Create(commonOptions),
+ Options.Create(dotNetCoreScriptGeneratorOptions),
+ installer,
+ globalJsonSdkResolver,
+ new TestExternalSdkProvider(),
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
+ }
}
}
diff --git a/tests/BuildScriptGenerator.Tests/Node/NodeBuildScriptGenerationTest.cs b/tests/BuildScriptGenerator.Tests/Node/NodeBuildScriptGenerationTest.cs
index 347d020f85..5a412f92e0 100644
--- a/tests/BuildScriptGenerator.Tests/Node/NodeBuildScriptGenerationTest.cs
+++ b/tests/BuildScriptGenerator.Tests/Node/NodeBuildScriptGenerationTest.cs
@@ -984,7 +984,10 @@ private static IProgrammingPlatform GetNodePlatform(
new TestEnvironment(),
new NodePlatformInstaller(Options.Create(commonOptions), NullLoggerFactory.Instance),
externalSdkProvider,
- TelemetryClientHelper.GetTelemetryClient());
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
}
private static BuildScriptGeneratorContext CreateScriptGeneratorContext(ISourceRepo sourceRepo)
diff --git a/tests/BuildScriptGenerator.Tests/Node/NodePlatformTest.cs b/tests/BuildScriptGenerator.Tests/Node/NodePlatformTest.cs
index c28e3d0ce9..a1015eea8b 100644
--- a/tests/BuildScriptGenerator.Tests/Node/NodePlatformTest.cs
+++ b/tests/BuildScriptGenerator.Tests/Node/NodePlatformTest.cs
@@ -452,6 +452,186 @@ public void BuildScript_UsesExternalProvider_IfDynamicInstallAndExternalProvider
Assert.Equal(TestNodePlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, buildScriptSnippet);
}
+ [Fact]
+ public void BuildScript_UsesExternalAcrProvider_IfEnabled_AndSdkIsNotAlreadyInstalled()
+ {
+ // Arrange
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: true);
+ var nodePlatform = CreateNodePlatformWithProviders(
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile(string.Empty, NodeConstants.PackageJsonFileName);
+ var context = CreateContext(repo);
+ var detectorResult = new NodePlatformDetectorResult
+ {
+ Platform = NodeConstants.PlatformName,
+ PlatformVersion = "20.14.0",
+ };
+
+ // Act
+ var buildScriptSnippet = nodePlatform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(buildScriptSnippet);
+ Assert.Equal(TestNodePlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, buildScriptSnippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ }
+
+ [Fact]
+ public void BuildScript_UsesDirectAcrProvider_IfEnabled_AndSdkIsNotAlreadyInstalled()
+ {
+ // Arrange
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var nodePlatform = CreateNodePlatformWithProviders(
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ acrSdkProvider: acrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile(string.Empty, NodeConstants.PackageJsonFileName);
+ var context = CreateContext(repo);
+ var detectorResult = new NodePlatformDetectorResult
+ {
+ Platform = NodeConstants.PlatformName,
+ PlatformVersion = "20.14.0",
+ };
+
+ // Act
+ var buildScriptSnippet = nodePlatform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(buildScriptSnippet);
+ Assert.Equal(TestNodePlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, buildScriptSnippet);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void BuildScript_FallsBackFromExternalAcrToExternalSdk_WhenExternalAcrFails()
+ {
+ // Arrange
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var nodePlatform = CreateNodePlatformWithProviders(
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile(string.Empty, NodeConstants.PackageJsonFileName);
+ var context = CreateContext(repo);
+ var detectorResult = new NodePlatformDetectorResult
+ {
+ Platform = NodeConstants.PlatformName,
+ PlatformVersion = "20.14.0",
+ };
+
+ // Act
+ var buildScriptSnippet = nodePlatform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(buildScriptSnippet);
+ Assert.Equal(TestNodePlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, buildScriptSnippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ }
+
+ [Fact]
+ public void BuildScript_FallsBackFromExternalAcrToDirectAcr_WhenBothExternalProvidersFail()
+ {
+ // Arrange
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var externalSdkProvider = new TestExternalSdkProvider(requestBlobResult: false);
+ var nodePlatform = CreateNodePlatformWithProviders(
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider,
+ acrSdkProvider: acrSdkProvider,
+ externalSdkProvider: externalSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile(string.Empty, NodeConstants.PackageJsonFileName);
+ var context = CreateContext(repo);
+ var detectorResult = new NodePlatformDetectorResult
+ {
+ Platform = NodeConstants.PlatformName,
+ PlatformVersion = "20.14.0",
+ };
+
+ // Act
+ var buildScriptSnippet = nodePlatform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(buildScriptSnippet);
+ Assert.Equal(TestNodePlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, buildScriptSnippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void BuildScript_FallsBackToCdn_WhenAllProvidersFail()
+ {
+ // Arrange
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: false);
+ var externalSdkProvider = new TestExternalSdkProvider(requestBlobResult: false);
+ var nodePlatform = CreateNodePlatformWithProviders(
+ commonOptions: commonOptions,
+ sdkAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider,
+ acrSdkProvider: acrSdkProvider,
+ externalSdkProvider: externalSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile(string.Empty, NodeConstants.PackageJsonFileName);
+ var context = CreateContext(repo);
+ var detectorResult = new NodePlatformDetectorResult
+ {
+ Platform = NodeConstants.PlatformName,
+ PlatformVersion = "20.14.0",
+ };
+
+ // Act
+ var buildScriptSnippet = nodePlatform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(buildScriptSnippet);
+ Assert.Equal(TestNodePlatformInstaller.InstallerScript, buildScriptSnippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
[Fact]
public void BuildScript_HasNoSdkInstallScript_IfDynamicInstallIsEnabled_AndSdkIsAlreadyInstalled()
{
@@ -1000,6 +1180,33 @@ public void Detect_ReturnsResult_WithDefaultVersion_ForSourceRepoOnlyWithAppJs_A
Assert.Equal(expectedVersion, result.PlatformVersion);
}
+ [Fact]
+ public void Detect_ReturnsVersionProviderDefault_WhenExternalAcrEnabled_OverridingDetectedVersion()
+ {
+ // Arrange
+ var detectedVersion = "14.0.0";
+ var versionProviderDefault = "18.0.0";
+ var commonOptions = new BuildScriptGeneratorOptions
+ {
+ EnableExternalAcrSdkProvider = true,
+ };
+ var platform = CreateNodePlatform(
+ supportedNodeVersions: new[] { detectedVersion, versionProviderDefault },
+ defaultVersion: versionProviderDefault,
+ detectedVersion: detectedVersion,
+ commonOptions: commonOptions);
+ var context = CreateContext();
+
+ // Act
+ var result = platform.Detect(context);
+
+ // Assert - ExternalACR short-circuit uses version provider's DefaultVersion,
+ // overriding the detected version
+ Assert.NotNull(result);
+ Assert.Equal(NodeConstants.PlatformName, result.Platform);
+ Assert.Equal(versionProviderDefault, result.PlatformVersion);
+ }
+
private TestNodePlatform CreateNodePlatform(
string[] supportedNodeVersions = null,
string defaultVersion = null,
@@ -1025,7 +1232,10 @@ private TestNodePlatform CreateNodePlatform(
environment,
platformInstaller,
externalSdkProvider,
- TelemetryClientHelper.GetTelemetryClient());
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
}
private TestNodePlatform CreateNodePlatform(
@@ -1049,7 +1259,10 @@ private TestNodePlatform CreateNodePlatform(
environment,
platformInstaller,
externalSdkProvider,
- TelemetryClientHelper.GetTelemetryClient());
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
}
private TestNodePlatform CreateNodePlatform(
@@ -1077,7 +1290,44 @@ private TestNodePlatform CreateNodePlatform(
environment,
installer,
externalSdkProvider,
- TelemetryClientHelper.GetTelemetryClient());
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
+ }
+
+ private TestNodePlatform CreateNodePlatformWithProviders(
+ BuildScriptGeneratorOptions commonOptions,
+ bool sdkAlreadyInstalled,
+ IExternalAcrSdkProvider externalAcrSdkProvider = null,
+ IAcrSdkProvider acrSdkProvider = null,
+ IExternalSdkProvider externalSdkProvider = null)
+ {
+ commonOptions.EnableDynamicInstall = true;
+ var environment = new TestEnvironment();
+ var installer = new TestNodePlatformInstaller(
+ Options.Create(commonOptions),
+ sdkAlreadyInstalled,
+ NullLoggerFactory.Instance);
+ var versionProvider = new TestNodeVersionProvider();
+ externalSdkProvider = externalSdkProvider ?? new TestExternalSdkProvider();
+ externalAcrSdkProvider = externalAcrSdkProvider ?? new TestExternalAcrSdkProvider();
+ acrSdkProvider = acrSdkProvider ?? new TestAcrSdkProvider();
+ var nodeScriptGeneratorOptions = new NodeScriptGeneratorOptions();
+ var detector = new TestNodePlatformDetector();
+ return new TestNodePlatform(
+ Options.Create(commonOptions),
+ Options.Create(nodeScriptGeneratorOptions),
+ versionProvider,
+ NullLogger.Instance,
+ detector,
+ environment,
+ installer,
+ externalSdkProvider,
+ externalAcrSdkProvider,
+ acrSdkProvider,
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
}
private BuildScriptGeneratorContext CreateContext(ISourceRepo sourceRepo = null)
@@ -1101,7 +1351,10 @@ public TestNodePlatform(
IEnvironment environment,
NodePlatformInstaller nodePlatformInstaller,
IExternalSdkProvider externalSdkProvider,
- TelemetryClient telemetryClient)
+ IExternalAcrSdkProvider externalAcrSdkProvider,
+ IAcrSdkProvider acrSdkProvider,
+ TelemetryClient telemetryClient,
+ IStandardOutputWriter outputWriter)
: base(
cliOptions,
nodeScriptGeneratorOptions,
@@ -1111,7 +1364,10 @@ public TestNodePlatform(
environment,
nodePlatformInstaller,
externalSdkProvider,
- telemetryClient)
+ externalAcrSdkProvider,
+ acrSdkProvider,
+ telemetryClient,
+ outputWriter)
{
}
}
diff --git a/tests/BuildScriptGenerator.Tests/Node/NodeVersionProviderTest.cs b/tests/BuildScriptGenerator.Tests/Node/NodeVersionProviderTest.cs
index f3080bc8c8..e5313a9f90 100644
--- a/tests/BuildScriptGenerator.Tests/Node/NodeVersionProviderTest.cs
+++ b/tests/BuildScriptGenerator.Tests/Node/NodeVersionProviderTest.cs
@@ -1,4 +1,4 @@
-// --------------------------------------------------------------------------------------------
+// --------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
@@ -7,6 +7,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator;
using Microsoft.Oryx.BuildScriptGenerator.Node;
using Microsoft.Oryx.Tests.Common;
using Xunit;
@@ -79,6 +80,215 @@ public void GetsVersions_UsesExternalVersionProvider_WhenExternalProviderAndDyna
Assert.False(onDiskVersionProvider.GetVersionInfoCalled);
}
+ [Fact]
+ public void GetsVersions_UsesExternalAcrProvider_WhenExternalAcrProviderAndDynamicInstallEnabled()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.OnDiskVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_UsesAcrProvider_WhenAcrProviderAndDynamicInstallEnabled()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableAcrSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.OnDiskVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_ExternalAcrTakesPriority_OverExternalSdk()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_ExternalAcrTakesPriority_OverDirectAcr()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableAcrSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToStorage_WhenExternalAcrReturnsNull()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ externalAcrReturnsNull: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToStorage_WhenAcrProviderThrows()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableAcrSdkProvider: true,
+ acrThrowsException: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToDirectAcr_WhenExternalAcrAndExternalSdkReturnNull()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true,
+ enableAcrSdkProvider: true,
+ externalAcrReturnsNull: true,
+ externalSdkReturnsNull: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToCdn_WhenAllProvidersReturnNull()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true,
+ enableAcrSdkProvider: true,
+ externalAcrReturnsNull: true,
+ externalSdkReturnsNull: true,
+ acrThrowsException: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToExternalSdk_WhenExternalAcrThrows()
+ {
+ // Arrange — ExternalACR throws, should fall to ExternalSDK
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true,
+ externalAcrThrowsException: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToDirectAcr_WhenExternalAcrAndExternalSdkThrow()
+ {
+ // Arrange — both external providers throw, should fall to direct ACR
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true,
+ enableAcrSdkProvider: true,
+ externalAcrThrowsException: true,
+ externalSdkThrowsException: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_CachesResult_OnSecondCall()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableAcrSdkProvider: true);
+
+ // Act — call twice
+ var versionInfo1 = result.VersionProvider.GetVersionInfo();
+ var versionInfo2 = result.VersionProvider.GetVersionInfo();
+
+ // Assert — same instance returned (cached)
+ Assert.Same(versionInfo1, versionInfo2);
+ }
+
private class TestNodeSdkStorageVersionProvider : NodeSdkStorageVersionProvider
{
public TestNodeSdkStorageVersionProvider(
@@ -99,10 +309,16 @@ public override PlatformVersionInfo GetVersionInfo()
private class TestNodeExternalVersionProvider : NodeExternalVersionProvider
{
+ private readonly bool _returnsNull;
+ private readonly bool _throwsException;
+
public TestNodeExternalVersionProvider(
- IOptions commonOptions, IExternalSdkProvider externalProvider, ILoggerFactory loggerFactory)
+ IOptions commonOptions, IExternalSdkProvider externalProvider, ILoggerFactory loggerFactory,
+ bool returnsNull = false, bool throwsException = false)
: base(commonOptions, externalProvider, loggerFactory)
{
+ _returnsNull = returnsNull;
+ _throwsException = throwsException;
}
public bool GetVersionInfoCalled { get; private set; }
@@ -110,7 +326,18 @@ public TestNodeExternalVersionProvider(
public override PlatformVersionInfo GetVersionInfo()
{
GetVersionInfoCalled = true;
- return null;
+ if (_throwsException)
+ {
+ throw new System.Exception("External SDK provider simulated failure");
+ }
+
+ if (_returnsNull)
+ {
+ return null;
+ }
+
+ return PlatformVersionInfo.CreateAvailableViaExternalProvider(
+ new[] { "1.0.0" }, "1.0.0");
}
}
@@ -137,7 +364,10 @@ public override PlatformVersionInfo GetVersionInfo()
onDiskProvider,
storageProvider,
externalProvider,
- NullLogger.Instance);
+ new NodeExternalAcrVersionProvider(Options.Create(new BuildScriptGeneratorOptions()), NullLoggerFactory.Instance, new DefaultStandardOutputWriter()),
+ new NodeAcrVersionProvider(commonOptions, new OciRegistryClient("https://test.azurecr.io", new TestHttpClientFactory(), NullLoggerFactory.Instance), NullLoggerFactory.Instance),
+ NullLogger.Instance,
+ new DefaultStandardOutputWriter());
return (versionProvider, onDiskProvider, storageProvider, externalProvider);
}
@@ -157,5 +387,148 @@ public override PlatformVersionInfo GetVersionInfo()
return null;
}
}
+
+ private class TestNodeExternalAcrVersionProvider : NodeExternalAcrVersionProvider
+ {
+ private readonly bool _returnsNull;
+ private readonly bool _throwsException;
+
+ public TestNodeExternalAcrVersionProvider(
+ IOptions options,
+ ILoggerFactory loggerFactory,
+ IStandardOutputWriter outputWriter,
+ bool returnsNull = false,
+ bool throwsException = false)
+ : base(options, loggerFactory, outputWriter)
+ {
+ _returnsNull = returnsNull;
+ _throwsException = throwsException;
+ }
+
+ public bool GetVersionInfoCalled { get; private set; }
+
+ public override PlatformVersionInfo GetVersionInfo()
+ {
+ GetVersionInfoCalled = true;
+ if (_throwsException)
+ {
+ throw new System.Exception("External ACR provider simulated failure");
+ }
+
+ if (_returnsNull)
+ {
+ return null;
+ }
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(
+ new[] { "18.0.0" }, "18.0.0");
+ }
+ }
+
+ private class TestNodeAcrVersionProvider : NodeAcrVersionProvider
+ {
+ private readonly bool _throwsException;
+
+ public TestNodeAcrVersionProvider(
+ IOptions commonOptions,
+ OciRegistryClient ociClient,
+ ILoggerFactory loggerFactory,
+ bool throwsException = false)
+ : base(commonOptions, ociClient, loggerFactory)
+ {
+ _throwsException = throwsException;
+ }
+
+ public bool GetVersionInfoCalled { get; private set; }
+
+ public override PlatformVersionInfo GetVersionInfo()
+ {
+ GetVersionInfoCalled = true;
+ if (_throwsException)
+ {
+ throw new System.Exception("ACR provider simulated failure");
+ }
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(
+ new[] { "18.0.0" }, "18.0.0");
+ }
+ }
+
+ private class VersionProviderResult
+ {
+ public INodeVersionProvider VersionProvider { get; set; }
+
+ public TestNodeOnDiskVersionProvider OnDiskVersionProvider { get; set; }
+
+ public TestNodeSdkStorageVersionProvider StorageVersionProvider { get; set; }
+
+ public TestNodeExternalVersionProvider ExternalVersionProvider { get; set; }
+
+ public TestNodeExternalAcrVersionProvider ExternalAcrVersionProvider { get; set; }
+
+ public TestNodeAcrVersionProvider AcrVersionProvider { get; set; }
+ }
+
+ private VersionProviderResult CreateVersionProviderWithAcr(
+ bool enableDynamicInstall,
+ bool enableExternalSdkProvider = false,
+ bool enableExternalAcrSdkProvider = false,
+ bool enableAcrSdkProvider = false,
+ bool externalAcrReturnsNull = false,
+ bool externalSdkReturnsNull = false,
+ bool acrThrowsException = false,
+ bool externalAcrThrowsException = false,
+ bool externalSdkThrowsException = false)
+ {
+ var commonOptions = Options.Create(new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = enableDynamicInstall,
+ EnableExternalSdkProvider = enableExternalSdkProvider,
+ EnableExternalAcrSdkProvider = enableExternalAcrSdkProvider,
+ EnableAcrSdkProvider = enableAcrSdkProvider,
+ });
+
+ var onDiskProvider = new TestNodeOnDiskVersionProvider(commonOptions);
+ var storageProvider = new TestNodeSdkStorageVersionProvider(
+ commonOptions,
+ new TestHttpClientFactory(),
+ NullLoggerFactory.Instance);
+ var externalProvider = new TestNodeExternalVersionProvider(
+ commonOptions,
+ new TestExternalSdkProvider(),
+ NullLoggerFactory.Instance,
+ returnsNull: externalSdkReturnsNull,
+ throwsException: externalSdkThrowsException);
+ var externalAcrProvider = new TestNodeExternalAcrVersionProvider(
+ commonOptions,
+ NullLoggerFactory.Instance,
+ new DefaultStandardOutputWriter(),
+ returnsNull: externalAcrReturnsNull,
+ throwsException: externalAcrThrowsException);
+ var acrProvider = new TestNodeAcrVersionProvider(
+ commonOptions,
+ new OciRegistryClient("https://test.azurecr.io", new TestHttpClientFactory(), NullLoggerFactory.Instance),
+ NullLoggerFactory.Instance,
+ throwsException: acrThrowsException);
+ var versionProvider = new NodeVersionProvider(
+ commonOptions,
+ onDiskProvider,
+ storageProvider,
+ externalProvider,
+ externalAcrProvider,
+ acrProvider,
+ NullLogger.Instance,
+ new DefaultStandardOutputWriter());
+
+ return new VersionProviderResult
+ {
+ VersionProvider = versionProvider,
+ OnDiskVersionProvider = onDiskProvider,
+ StorageVersionProvider = storageProvider,
+ ExternalVersionProvider = externalProvider,
+ ExternalAcrVersionProvider = externalAcrProvider,
+ AcrVersionProvider = acrProvider,
+ };
+ }
}
}
diff --git a/tests/BuildScriptGenerator.Tests/OciRegistryClientTest.cs b/tests/BuildScriptGenerator.Tests/OciRegistryClientTest.cs
new file mode 100644
index 0000000000..b98ac67ee0
--- /dev/null
+++ b/tests/BuildScriptGenerator.Tests/OciRegistryClientTest.cs
@@ -0,0 +1,403 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Tests
+{
+ public class OciRegistryClientTest
+ {
+ // --- Constructor validation ---
+
+ [Fact]
+ public void Constructor_ThrowsForEmptyRegistryUrl()
+ {
+ var ex = Assert.Throws(
+ () => new OciRegistryClient(string.Empty, new MockHttpClientFactory(), NullLoggerFactory.Instance));
+ Assert.Contains("Registry URL", ex.Message);
+ }
+
+ [Fact]
+ public void Constructor_ThrowsForNonHttpsUrl()
+ {
+ var ex = Assert.Throws(
+ () => new OciRegistryClient("http://insecure.io", new MockHttpClientFactory(), NullLoggerFactory.Instance));
+ Assert.Contains("HTTPS", ex.Message);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsValidHttpsUrl()
+ {
+ var client = new OciRegistryClient(
+ "https://myregistry.azurecr.io",
+ new MockHttpClientFactory(),
+ NullLoggerFactory.Instance);
+ Assert.NotNull(client);
+ }
+
+ // --- GetFirstLayerDigest ---
+
+ [Fact]
+ public void GetFirstLayerDigest_ReturnsNull_WhenManifestIsNull()
+ {
+ Assert.Null(OciRegistryClient.GetFirstLayerDigest(null));
+ }
+
+ [Fact]
+ public void GetFirstLayerDigest_ReturnsNull_WhenLayersAreEmpty()
+ {
+ var manifest = new OciManifest { Layers = new List() };
+ Assert.Null(OciRegistryClient.GetFirstLayerDigest(manifest));
+ }
+
+ [Fact]
+ public void GetFirstLayerDigest_ReturnsNull_WhenLayersAreNull()
+ {
+ var manifest = new OciManifest { Layers = null };
+ Assert.Null(OciRegistryClient.GetFirstLayerDigest(manifest));
+ }
+
+ [Fact]
+ public void GetFirstLayerDigest_ReturnsFirstDigest()
+ {
+ var manifest = new OciManifest
+ {
+ Layers = new List
+ {
+ new OciDescriptor { Digest = "sha256:abc123" },
+ new OciDescriptor { Digest = "sha256:def456" },
+ },
+ };
+ Assert.Equal("sha256:abc123", OciRegistryClient.GetFirstLayerDigest(manifest));
+ }
+
+ // --- GetAllTagsAsync ---
+
+ [Fact]
+ public async Task GetAllTagsAsync_ReturnsTags_FromSinglePage()
+ {
+ var tagList = new OciTagList { Name = "test/repo", Tags = new List { "v1", "v2" } };
+ var handler = new MockHttpMessageHandler();
+ handler.AddResponse(
+ "https://myregistry.azurecr.io/v2/test/repo/tags/list",
+ new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(tagList)),
+ });
+
+ var client = CreateClient(handler);
+ var tags = await client.GetAllTagsAsync("test/repo");
+ Assert.Equal(new List { "v1", "v2" }, tags);
+ }
+
+ [Fact]
+ public async Task GetAllTagsAsync_HandlesPagination()
+ {
+ var page1 = new OciTagList { Name = "test/repo", Tags = new List { "v1" } };
+ var page2 = new OciTagList { Name = "test/repo", Tags = new List { "v2" } };
+
+ var handler = new MockHttpMessageHandler();
+ var resp1 = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(page1)),
+ };
+ resp1.Headers.Add("Link", "; rel=\"next\"");
+ handler.AddResponse("https://myregistry.azurecr.io/v2/test/repo/tags/list", resp1);
+ handler.AddResponse(
+ "https://myregistry.azurecr.io/v2/test/repo/tags/list?last=v1",
+ new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(page2)),
+ });
+
+ var client = CreateClient(handler);
+ var tags = await client.GetAllTagsAsync("test/repo");
+ Assert.Equal(new List { "v1", "v2" }, tags);
+ }
+
+ [Fact]
+ public async Task GetAllTagsAsync_ThrowsOnFailure()
+ {
+ var handler = new MockHttpMessageHandler();
+ handler.AddResponse(
+ "https://myregistry.azurecr.io/v2/test/repo/tags/list",
+ new HttpResponseMessage(HttpStatusCode.NotFound));
+
+ var client = CreateClient(handler);
+ await Assert.ThrowsAsync(() => client.GetAllTagsAsync("test/repo"));
+ }
+
+ // --- GetManifestAsync ---
+
+ [Fact]
+ public async Task GetManifestAsync_ParsesManifest()
+ {
+ var manifest = new OciManifest
+ {
+ SchemaVersion = 2,
+ Layers = new List
+ {
+ new OciDescriptor { Digest = "sha256:layer1", Size = 1234 },
+ },
+ };
+
+ var handler = new MockHttpMessageHandler();
+ handler.AddResponse(
+ "https://myregistry.azurecr.io/v2/repo/manifests/latest",
+ new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(manifest)),
+ });
+
+ var client = CreateClient(handler);
+ var result = await client.GetManifestAsync("repo", "latest");
+ Assert.NotNull(result);
+ Assert.Equal(2, result.SchemaVersion);
+ Assert.Single(result.Layers);
+ Assert.Equal("sha256:layer1", result.Layers[0].Digest);
+ }
+
+ // --- Token TTL ---
+
+ [Fact]
+ public async Task GetAllTagsAsync_UsesTokenFromCache_WhenNotExpired()
+ {
+ var tagList = new OciTagList { Tags = new List { "v1" } };
+ var registryHost = "custom.azurecr.io";
+ var baseUrl = $"https://{registryHost}";
+
+ var handler = new MockHttpMessageHandler();
+ // Token endpoint returns token with 600s lifetime
+ handler.AddResponse(
+ $"{baseUrl}/oauth2/token",
+ new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("{\"access_token\":\"tok1\",\"expires_in\":600}"),
+ });
+ // First tags request
+ handler.AddResponse(
+ $"{baseUrl}/v2/repo/tags/list",
+ new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(tagList)),
+ });
+
+ var client = CreateClient(handler, registryHost: registryHost);
+
+ // First call — acquires token
+ await client.GetAllTagsAsync("repo");
+ int tokenCallsAfterFirst = handler.GetCallCount($"{baseUrl}/oauth2/token");
+
+ // Second tags response
+ handler.AddResponse(
+ $"{baseUrl}/v2/repo/tags/list",
+ new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(tagList)),
+ });
+
+ // Second call — should use cached token
+ await client.GetAllTagsAsync("repo");
+ int tokenCallsAfterSecond = handler.GetCallCount($"{baseUrl}/oauth2/token");
+
+ // Token endpoint should only have been called once
+ Assert.Equal(tokenCallsAfterFirst, tokenCallsAfterSecond);
+ }
+
+ // --- GetManifestDigestAsync ---
+
+ [Fact]
+ public async Task GetManifestDigestAsync_ReturnsDigest()
+ {
+ var handler = new MockHttpMessageHandler();
+ var resp = new HttpResponseMessage(HttpStatusCode.OK);
+ resp.Headers.Add("Docker-Content-Digest", "sha256:abc123");
+ handler.AddResponse("https://myregistry.azurecr.io/v2/repo/manifests/v1", resp);
+
+ var client = CreateClient(handler);
+ var digest = await client.GetManifestDigestAsync("repo", "v1");
+ Assert.Equal("sha256:abc123", digest);
+ }
+
+ [Fact]
+ public async Task GetManifestDigestAsync_ReturnsNull_OnFailure()
+ {
+ var handler = new MockHttpMessageHandler();
+ handler.AddResponse(
+ "https://myregistry.azurecr.io/v2/repo/manifests/v1",
+ new HttpResponseMessage(HttpStatusCode.NotFound));
+
+ var client = CreateClient(handler);
+ var digest = await client.GetManifestDigestAsync("repo", "v1");
+ Assert.Null(digest);
+ }
+
+ // --- Link header host validation ---
+
+ [Fact]
+ public async Task GetAllTagsAsync_IgnoresLinkHeader_WhenHostDoesNotMatchRegistry()
+ {
+ var tagList = new OciTagList { Name = "test/repo", Tags = new List { "v1" } };
+ var handler = new MockHttpMessageHandler();
+ var resp = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(tagList)),
+ };
+ // Link header points to a different host
+ resp.Headers.Add("Link", "; rel=\"next\"");
+ handler.AddResponse("https://myregistry.azurecr.io/v2/test/repo/tags/list", resp);
+
+ var client = CreateClient(handler);
+ var tags = await client.GetAllTagsAsync("test/repo");
+
+ // Should only return page 1 tags — the malicious Link was ignored
+ Assert.Equal(new List { "v1" }, tags);
+ }
+
+ [Fact]
+ public async Task GetAllTagsAsync_FollowsRelativeLink()
+ {
+ var page1 = new OciTagList { Name = "test/repo", Tags = new List { "v1" } };
+ var page2 = new OciTagList { Name = "test/repo", Tags = new List { "v2" } };
+
+ var handler = new MockHttpMessageHandler();
+ var resp1 = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(page1)),
+ };
+ // Relative link (no host) — should be prefixed with registry URL
+ resp1.Headers.Add("Link", "; rel=\"next\"");
+ handler.AddResponse("https://myregistry.azurecr.io/v2/test/repo/tags/list", resp1);
+ handler.AddResponse(
+ "https://myregistry.azurecr.io/v2/test/repo/tags/list?last=v1",
+ new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(page2)),
+ });
+
+ var client = CreateClient(handler);
+ var tags = await client.GetAllTagsAsync("test/repo");
+ Assert.Equal(new List { "v1", "v2" }, tags);
+ }
+
+ // --- MCR skip-auth ---
+
+ [Fact]
+ public async Task GetAllTagsAsync_SkipsAuth_ForMcrRegistry()
+ {
+ var tagList = new OciTagList { Name = "oryx/nodejs-sdk", Tags = new List { "bookworm-20.19.3" } };
+ var handler = new MockHttpMessageHandler();
+ handler.AddResponse(
+ "https://mcr.microsoft.com/v2/oryx/nodejs-sdk/tags/list",
+ new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(tagList)),
+ });
+
+ var client = CreateClient(handler, registryHost: "mcr.microsoft.com");
+ var tags = await client.GetAllTagsAsync("oryx/nodejs-sdk");
+
+ // Should succeed without calling token endpoint
+ Assert.Equal(new List { "bookworm-20.19.3" }, tags);
+ Assert.Equal(0, handler.GetCallCount("https://mcr.microsoft.com/oauth2/token"));
+ }
+
+ // --- Helpers ---
+
+ private static OciRegistryClient CreateClient(
+ MockHttpMessageHandler handler,
+ string registryHost = "myregistry.azurecr.io")
+ {
+ var url = $"https://{registryHost}";
+ var factory = new MockHttpClientFactory(handler);
+ return new OciRegistryClient(url, factory, NullLoggerFactory.Instance);
+ }
+
+ private class MockHttpClientFactory : IHttpClientFactory
+ {
+ private readonly HttpMessageHandler _handler;
+
+ public MockHttpClientFactory()
+ {
+ _handler = new MockHttpMessageHandler();
+ }
+
+ public MockHttpClientFactory(HttpMessageHandler handler)
+ {
+ _handler = handler;
+ }
+
+ public HttpClient CreateClient(string name)
+ {
+ return new HttpClient(_handler);
+ }
+ }
+
+ private class MockHttpMessageHandler : HttpMessageHandler
+ {
+ private readonly Dictionary> _responses
+ = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ private readonly Dictionary _callCounts
+ = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ public void AddResponse(string urlPrefix, HttpResponseMessage response)
+ {
+ if (!_responses.ContainsKey(urlPrefix))
+ {
+ _responses[urlPrefix] = new Queue();
+ }
+
+ _responses[urlPrefix].Enqueue(response);
+ }
+
+ public int GetCallCount(string urlPrefix)
+ {
+ foreach (var kvp in _callCounts)
+ {
+ if (kvp.Key.StartsWith(urlPrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return kvp.Value;
+ }
+ }
+
+ return 0;
+ }
+
+ protected override Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var url = request.RequestUri.ToString();
+
+ // Track call counts
+ if (!_callCounts.ContainsKey(url))
+ {
+ _callCounts[url] = 0;
+ }
+
+ _callCounts[url]++;
+
+ // Find matching response by URL prefix
+ foreach (var kvp in _responses)
+ {
+ if (url.StartsWith(kvp.Key, StringComparison.OrdinalIgnoreCase) && kvp.Value.Count > 0)
+ {
+ return Task.FromResult(kvp.Value.Dequeue());
+ }
+ }
+
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
+ }
+ }
+ }
+}
diff --git a/tests/BuildScriptGenerator.Tests/Php/PhpPlatformTest.cs b/tests/BuildScriptGenerator.Tests/Php/PhpPlatformTest.cs
index 57f06f0c92..ab3aefa6d0 100644
--- a/tests/BuildScriptGenerator.Tests/Php/PhpPlatformTest.cs
+++ b/tests/BuildScriptGenerator.Tests/Php/PhpPlatformTest.cs
@@ -520,6 +520,401 @@ public void HasPhpAndComposerInstallScript_IfDynamicInstallIsEnabled_AndPhpAndCo
Assert.Contains(TestPhpComposerInstaller.InstallerScript, actualScriptSnippet);
}
+ [Fact]
+ public void PhpInstallViaExternalAcrProvider_IfEnabled_AndPhpVersionIsNotAlreadyInstalled()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: true);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var phpPlatform = CreatePhpPlatformWithProviders(
+ commonOptions: commonOptions,
+ isPhpVersionAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("{}", PhpConstants.ComposerFileName);
+ var context = CreateContext(repo);
+ var detectedResult = new PhpPlatformDetectorResult
+ {
+ Platform = PhpConstants.PlatformName,
+ PlatformVersion = "7.3.5",
+ };
+
+ // Act
+ var actualScriptSnippet = phpPlatform.GetInstallerScriptSnippet(context, detectedResult);
+
+ // Assert
+ Assert.NotNull(actualScriptSnippet);
+ Assert.Contains(TestPhpPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ }
+
+ [Fact]
+ public void PhpInstallViaDirectAcrProvider_IfEnabled_AndPhpVersionIsNotAlreadyInstalled()
+ {
+ // Arrange
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var phpPlatform = CreatePhpPlatformWithProviders(
+ commonOptions: commonOptions,
+ isPhpVersionAlreadyInstalled: false,
+ acrSdkProvider: acrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("{}", PhpConstants.ComposerFileName);
+ var context = CreateContext(repo);
+ var detectedResult = new PhpPlatformDetectorResult
+ {
+ Platform = PhpConstants.PlatformName,
+ PlatformVersion = "7.3.5",
+ };
+
+ // Act
+ var actualScriptSnippet = phpPlatform.GetInstallerScriptSnippet(context, detectedResult);
+
+ // Assert
+ Assert.NotNull(actualScriptSnippet);
+ Assert.Contains(TestPhpPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void PhpFallsBackFromExternalAcrToExternalSdk_WhenExternalAcrFails()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var phpPlatform = CreatePhpPlatformWithProviders(
+ commonOptions: commonOptions,
+ isPhpVersionAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("{}", PhpConstants.ComposerFileName);
+ var context = CreateContext(repo);
+ var detectedResult = new PhpPlatformDetectorResult
+ {
+ Platform = PhpConstants.PlatformName,
+ PlatformVersion = "7.3.5",
+ };
+
+ // Act
+ var actualScriptSnippet = phpPlatform.GetInstallerScriptSnippet(context, detectedResult);
+
+ // Assert
+ Assert.NotNull(actualScriptSnippet);
+ Assert.Contains(TestPhpPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ }
+
+ [Fact]
+ public void PhpFallsBackToCdn_WhenAllProvidersFail()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: false);
+ var externalSdkProvider = new TestExternalSdkProvider(requestBlobResult: false);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var phpPlatform = CreatePhpPlatformWithProviders(
+ commonOptions: commonOptions,
+ isPhpVersionAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider,
+ acrSdkProvider: acrSdkProvider,
+ externalSdkProvider: externalSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("{}", PhpConstants.ComposerFileName);
+ var context = CreateContext(repo);
+ var detectedResult = new PhpPlatformDetectorResult
+ {
+ Platform = PhpConstants.PlatformName,
+ PlatformVersion = "7.3.5",
+ };
+
+ // Act
+ var actualScriptSnippet = phpPlatform.GetInstallerScriptSnippet(context, detectedResult);
+
+ // Assert
+ Assert.NotNull(actualScriptSnippet);
+ Assert.Contains(TestPhpPlatformInstaller.InstallerScript, actualScriptSnippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void PhpFallsBackFromExternalAcrToDirectAcr_WhenBothExternalProvidersFail()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var externalSdkProvider = new TestExternalSdkProvider(requestBlobResult: false);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var phpPlatform = CreatePhpPlatformWithProviders(
+ commonOptions: commonOptions,
+ isPhpVersionAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider,
+ acrSdkProvider: acrSdkProvider,
+ externalSdkProvider: externalSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("{}", PhpConstants.ComposerFileName);
+ var context = CreateContext(repo);
+ var detectedResult = new PhpPlatformDetectorResult
+ {
+ Platform = PhpConstants.PlatformName,
+ PlatformVersion = "7.3.5",
+ };
+
+ // Act
+ var actualScriptSnippet = phpPlatform.GetInstallerScriptSnippet(context, detectedResult);
+
+ // Assert
+ Assert.NotNull(actualScriptSnippet);
+ Assert.Contains(TestPhpPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void ComposerInstallViaExternalAcrProvider_IfEnabled_AndComposerVersionIsNotAlreadyInstalled()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: true);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var phpPlatform = CreatePhpPlatformWithProviders(
+ commonOptions: commonOptions,
+ isPhpVersionAlreadyInstalled: false,
+ isPhpComposerAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("{}", PhpConstants.ComposerFileName);
+ var context = CreateContext(repo);
+ var detectedResult = new PhpPlatformDetectorResult
+ {
+ Platform = PhpConstants.PlatformName,
+ PlatformVersion = "7.3.5",
+ };
+
+ // Act
+ var actualScriptSnippet = phpPlatform.GetInstallerScriptSnippet(context, detectedResult);
+
+ // Assert
+ Assert.NotNull(actualScriptSnippet);
+ Assert.Contains(TestPhpPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ Assert.Contains(TestPhpComposerInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ }
+
+ [Fact]
+ public void ComposerInstallViaDirectAcrProvider_IfEnabled_AndComposerVersionIsNotAlreadyInstalled()
+ {
+ // Arrange
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var phpPlatform = CreatePhpPlatformWithProviders(
+ commonOptions: commonOptions,
+ isPhpVersionAlreadyInstalled: false,
+ isPhpComposerAlreadyInstalled: false,
+ acrSdkProvider: acrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("{}", PhpConstants.ComposerFileName);
+ var context = CreateContext(repo);
+ var detectedResult = new PhpPlatformDetectorResult
+ {
+ Platform = PhpConstants.PlatformName,
+ PlatformVersion = "7.3.5",
+ };
+
+ // Act
+ var actualScriptSnippet = phpPlatform.GetInstallerScriptSnippet(context, detectedResult);
+
+ // Assert
+ Assert.NotNull(actualScriptSnippet);
+ Assert.Contains(TestPhpPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ Assert.Contains(TestPhpComposerInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ }
+
+ [Fact]
+ public void ComposerFallsBackToCdn_WhenAllProvidersFail()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: false);
+ var externalSdkProvider = new TestExternalSdkProvider(requestBlobResult: false);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var phpPlatform = CreatePhpPlatformWithProviders(
+ commonOptions: commonOptions,
+ isPhpVersionAlreadyInstalled: false,
+ isPhpComposerAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider,
+ acrSdkProvider: acrSdkProvider,
+ externalSdkProvider: externalSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("{}", PhpConstants.ComposerFileName);
+ var context = CreateContext(repo);
+ var detectedResult = new PhpPlatformDetectorResult
+ {
+ Platform = PhpConstants.PlatformName,
+ PlatformVersion = "7.3.5",
+ };
+
+ // Act
+ var actualScriptSnippet = phpPlatform.GetInstallerScriptSnippet(context, detectedResult);
+
+ // Assert
+ Assert.NotNull(actualScriptSnippet);
+ Assert.Contains(TestPhpPlatformInstaller.InstallerScript, actualScriptSnippet);
+ Assert.Contains(TestPhpComposerInstaller.InstallerScript, actualScriptSnippet);
+ }
+
+ [Fact]
+ public void ComposerFallsBackFromExternalAcrToExternalSdk_WhenExternalAcrFails()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var phpPlatform = CreatePhpPlatformWithProviders(
+ commonOptions: commonOptions,
+ isPhpVersionAlreadyInstalled: false,
+ isPhpComposerAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("{}", PhpConstants.ComposerFileName);
+ var context = CreateContext(repo);
+ var detectedResult = new PhpPlatformDetectorResult
+ {
+ Platform = PhpConstants.PlatformName,
+ PlatformVersion = "7.3.5",
+ };
+
+ // Act
+ var actualScriptSnippet = phpPlatform.GetInstallerScriptSnippet(context, detectedResult);
+
+ // Assert - Both PHP and Composer should fall from ExternalACR to ExternalSDK
+ Assert.NotNull(actualScriptSnippet);
+ Assert.Contains(TestPhpPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ Assert.Contains(TestPhpComposerInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ }
+
+ [Fact]
+ public void ComposerFallsBackFromExternalAcrToDirectAcr_WhenBothExternalProvidersFail()
+ {
+ // Arrange
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var externalSdkProvider = new TestExternalSdkProvider(requestBlobResult: false);
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var phpPlatform = CreatePhpPlatformWithProviders(
+ commonOptions: commonOptions,
+ isPhpVersionAlreadyInstalled: false,
+ isPhpComposerAlreadyInstalled: false,
+ externalAcrSdkProvider: externalAcrSdkProvider,
+ acrSdkProvider: acrSdkProvider,
+ externalSdkProvider: externalSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("{}", PhpConstants.ComposerFileName);
+ var context = CreateContext(repo);
+ var detectedResult = new PhpPlatformDetectorResult
+ {
+ Platform = PhpConstants.PlatformName,
+ PlatformVersion = "7.3.5",
+ };
+
+ // Act
+ var actualScriptSnippet = phpPlatform.GetInstallerScriptSnippet(context, detectedResult);
+
+ // Assert - Both PHP and Composer should fall from ExternalACR+ExternalSDK to DirectACR
+ Assert.NotNull(actualScriptSnippet);
+ Assert.Contains(TestPhpPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ Assert.Contains(TestPhpComposerInstaller.InstallerScriptWithSkipSdkBinaryDownload, actualScriptSnippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void Detect_ReturnsVersionProviderDefault_WhenExternalAcrEnabled_OverridingDetectedVersion()
+ {
+ // Arrange
+ var detectedVersion = "7.3.5";
+ var versionProviderDefault = "8.1.0";
+ var repo = new MemorySourceRepo();
+ repo.AddFile("", "foo.php");
+ var context = CreateContext(repo);
+ var commonOptions = new BuildScriptGeneratorOptions
+ {
+ EnableExternalAcrSdkProvider = true,
+ };
+ var platform = CreatePhpPlatform(
+ supportedPhpVersions: new[] { detectedVersion, versionProviderDefault },
+ defaultVersion: versionProviderDefault,
+ detectedVersion: detectedVersion,
+ commonOptions: commonOptions);
+
+ // Act
+ var result = platform.Detect(context);
+
+ // Assert - ExternalACR short-circuit uses version provider's DefaultVersion,
+ // overriding the detected version
+ Assert.NotNull(result);
+ Assert.Equal(PhpConstants.PlatformName, result.Platform);
+ Assert.Equal(versionProviderDefault, result.PlatformVersion);
+ }
+
[Theory]
[InlineData(null, "7.4.30", null, "7.4.30")]
[InlineData(null, "7.4.30", "7.3.1", "7.4.30")]
@@ -591,7 +986,52 @@ private PhpPlatform CreatePhpPlatform(
phpInstaller,
phpComposerInstaller,
externalSdkProvider,
- TelemetryClientHelper.GetTelemetryClient());
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
+ }
+
+ private PhpPlatform CreatePhpPlatformWithProviders(
+ BuildScriptGeneratorOptions commonOptions = null,
+ bool? isPhpVersionAlreadyInstalled = null,
+ bool? isPhpComposerAlreadyInstalled = null,
+ IExternalAcrSdkProvider externalAcrSdkProvider = null,
+ IAcrSdkProvider acrSdkProvider = null,
+ IExternalSdkProvider externalSdkProvider = null)
+ {
+ commonOptions = commonOptions ?? new BuildScriptGeneratorOptions();
+ var phpScriptGeneratorOptions = new PhpScriptGeneratorOptions();
+ isPhpVersionAlreadyInstalled = isPhpVersionAlreadyInstalled ?? true;
+ isPhpComposerAlreadyInstalled = isPhpComposerAlreadyInstalled ?? true;
+ var versionProvider = new TestPhpVersionProvider(null, null);
+ externalSdkProvider = externalSdkProvider ?? new TestExternalSdkProvider();
+ externalAcrSdkProvider = externalAcrSdkProvider ?? new TestExternalAcrSdkProvider();
+ acrSdkProvider = acrSdkProvider ?? new TestAcrSdkProvider();
+ var composerVersionProvider = new TestPhpComposerVersionProvider(
+ new[] { PhpVersions.ComposerDefaultVersion },
+ PhpVersions.ComposerDefaultVersion);
+ var detector = new TestPhpPlatformDetector(detectedVersion: null);
+ var phpInstaller = new TestPhpPlatformInstaller(
+ Options.Create(commonOptions),
+ isPhpVersionAlreadyInstalled.Value);
+ var phpComposerInstaller = new TestPhpComposerInstaller(
+ Options.Create(commonOptions),
+ isPhpComposerAlreadyInstalled.Value);
+ return new TestPhpPlatform(
+ Options.Create(phpScriptGeneratorOptions),
+ Options.Create(commonOptions),
+ versionProvider,
+ composerVersionProvider,
+ NullLogger.Instance,
+ detector,
+ phpInstaller,
+ phpComposerInstaller,
+ externalSdkProvider,
+ externalAcrSdkProvider,
+ acrSdkProvider,
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
}
private BuildScriptGeneratorContext CreateContext(ISourceRepo sourceRepo = null)
@@ -616,7 +1056,10 @@ public TestPhpPlatform(
PhpPlatformInstaller phpInstaller,
PhpComposerInstaller phpComposerInstaller,
IExternalSdkProvider externalSdkProvider,
- TelemetryClient telemetryClient)
+ IExternalAcrSdkProvider externalAcrSdkProvider,
+ IAcrSdkProvider acrSdkProvider,
+ TelemetryClient telemetryClient,
+ IStandardOutputWriter outputWriter)
: base(
phpScriptGeneratorOptions,
commonOptions,
@@ -627,7 +1070,10 @@ public TestPhpPlatform(
phpInstaller,
phpComposerInstaller,
externalSdkProvider,
- telemetryClient)
+ externalAcrSdkProvider,
+ acrSdkProvider,
+ telemetryClient,
+ outputWriter)
{
}
}
diff --git a/tests/BuildScriptGenerator.Tests/Php/PhpScriptGeneratorTest.cs b/tests/BuildScriptGenerator.Tests/Php/PhpScriptGeneratorTest.cs
index 958a40ea0a..96ba448803 100644
--- a/tests/BuildScriptGenerator.Tests/Php/PhpScriptGeneratorTest.cs
+++ b/tests/BuildScriptGenerator.Tests/Php/PhpScriptGeneratorTest.cs
@@ -146,7 +146,10 @@ private IProgrammingPlatform GetScriptGenerator(
phpInstaller: null,
phpComposerInstaller: null,
externalSdkProvider,
- TelemetryClientHelper.GetTelemetryClient());
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
}
private static BuildScriptGeneratorContext CreateBuildScriptGeneratorContext(ISourceRepo sourceRepo)
diff --git a/tests/BuildScriptGenerator.Tests/Php/PhpVersionProviderTest.cs b/tests/BuildScriptGenerator.Tests/Php/PhpVersionProviderTest.cs
index fe082faa6a..6f40781af4 100644
--- a/tests/BuildScriptGenerator.Tests/Php/PhpVersionProviderTest.cs
+++ b/tests/BuildScriptGenerator.Tests/Php/PhpVersionProviderTest.cs
@@ -1,4 +1,4 @@
-// --------------------------------------------------------------------------------------------
+// --------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
@@ -7,6 +7,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator;
using Microsoft.Oryx.BuildScriptGenerator.Php;
using Microsoft.Oryx.Tests.Common;
using Xunit;
@@ -79,6 +80,157 @@ public void GetsVersions_UsesExternalVersionProvider_WhenExternalProviderAndDyna
Assert.False(onDiskVersionProvider.GetVersionInfoCalled);
}
+ [Fact]
+ public void GetsVersions_UsesExternalAcrProvider_WhenExternalAcrProviderAndDynamicInstallEnabled()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.OnDiskVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_UsesAcrProvider_WhenAcrProviderAndDynamicInstallEnabled()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableAcrSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.OnDiskVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_ExternalAcrTakesPriority_OverExternalSdk()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_ExternalAcrTakesPriority_OverDirectAcr()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableAcrSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToStorage_WhenExternalAcrReturnsNull()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ externalAcrReturnsNull: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToStorage_WhenAcrProviderThrows()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableAcrSdkProvider: true,
+ acrThrowsException: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToDirectAcr_WhenExternalAcrAndExternalSdkReturnNull()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true,
+ enableAcrSdkProvider: true,
+ externalAcrReturnsNull: true,
+ externalSdkReturnsNull: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToCdn_WhenAllProvidersReturnNull()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true,
+ enableAcrSdkProvider: true,
+ externalAcrReturnsNull: true,
+ externalSdkReturnsNull: true,
+ acrThrowsException: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
private class TestPhpSdkStorageVersionProvider : PhpSdkStorageVersionProvider
{
public TestPhpSdkStorageVersionProvider(
@@ -101,10 +253,14 @@ public override PlatformVersionInfo GetVersionInfo()
private class TestPhpExternalVersionProvider : PhpExternalVersionProvider
{
+ private readonly bool _returnsNull;
+
public TestPhpExternalVersionProvider(
- IOptions commonOptions, IExternalSdkProvider externalProvider, ILoggerFactory loggerFactory)
+ IOptions commonOptions, IExternalSdkProvider externalProvider, ILoggerFactory loggerFactory,
+ bool returnsNull = false)
: base(commonOptions, externalProvider, loggerFactory)
{
+ _returnsNull = returnsNull;
}
public bool GetVersionInfoCalled { get; private set; }
@@ -112,8 +268,13 @@ public TestPhpExternalVersionProvider(
public override PlatformVersionInfo GetVersionInfo()
{
GetVersionInfoCalled = true;
+ if (_returnsNull)
+ {
+ return null;
+ }
- return null;
+ return PlatformVersionInfo.CreateAvailableViaExternalProvider(
+ new[] { "1.0.0" }, "1.0.0");
}
}
@@ -140,7 +301,10 @@ public override PlatformVersionInfo GetVersionInfo()
onDiskProvider,
storageProvider,
externalProvider,
- NullLogger.Instance);
+ new PhpExternalAcrVersionProvider(Options.Create(new BuildScriptGeneratorOptions()), NullLoggerFactory.Instance, new DefaultStandardOutputWriter()),
+ new PhpAcrVersionProvider(commonOptions, new OciRegistryClient("https://test.azurecr.io", new TestHttpClientFactory(), NullLoggerFactory.Instance), NullLoggerFactory.Instance),
+ NullLogger.Instance,
+ new DefaultStandardOutputWriter());
return (versionProvider, onDiskProvider, storageProvider, externalProvider);
}
@@ -160,5 +324,136 @@ public override PlatformVersionInfo GetVersionInfo()
return null;
}
}
+
+ private class TestPhpExternalAcrVersionProvider : PhpExternalAcrVersionProvider
+ {
+ private readonly bool _returnsNull;
+
+ public TestPhpExternalAcrVersionProvider(
+ IOptions options,
+ ILoggerFactory loggerFactory,
+ IStandardOutputWriter outputWriter,
+ bool returnsNull = false)
+ : base(options, loggerFactory, outputWriter)
+ {
+ _returnsNull = returnsNull;
+ }
+
+ public bool GetVersionInfoCalled { get; private set; }
+
+ public override PlatformVersionInfo GetVersionInfo()
+ {
+ GetVersionInfoCalled = true;
+ if (_returnsNull)
+ {
+ return null;
+ }
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(
+ new[] { "8.1.0" }, "8.1.0");
+ }
+ }
+
+ private class TestPhpAcrVersionProvider : PhpAcrVersionProvider
+ {
+ private readonly bool _throwsException;
+
+ public TestPhpAcrVersionProvider(
+ IOptions commonOptions,
+ OciRegistryClient ociClient,
+ ILoggerFactory loggerFactory,
+ bool throwsException = false)
+ : base(commonOptions, ociClient, loggerFactory)
+ {
+ _throwsException = throwsException;
+ }
+
+ public bool GetVersionInfoCalled { get; private set; }
+
+ public override PlatformVersionInfo GetVersionInfo()
+ {
+ GetVersionInfoCalled = true;
+ if (_throwsException)
+ {
+ throw new System.Exception("ACR provider simulated failure");
+ }
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(
+ new[] { "8.1.0" }, "8.1.0");
+ }
+ }
+
+ private class VersionProviderResult
+ {
+ public IPhpVersionProvider VersionProvider { get; set; }
+
+ public TestPhpOnDiskVersionProvider OnDiskVersionProvider { get; set; }
+
+ public TestPhpSdkStorageVersionProvider StorageVersionProvider { get; set; }
+
+ public TestPhpExternalVersionProvider ExternalVersionProvider { get; set; }
+
+ public TestPhpExternalAcrVersionProvider ExternalAcrVersionProvider { get; set; }
+
+ public TestPhpAcrVersionProvider AcrVersionProvider { get; set; }
+ }
+
+ private VersionProviderResult CreateVersionProviderWithAcr(
+ bool enableDynamicInstall,
+ bool enableExternalSdkProvider = false,
+ bool enableExternalAcrSdkProvider = false,
+ bool enableAcrSdkProvider = false,
+ bool externalAcrReturnsNull = false,
+ bool externalSdkReturnsNull = false,
+ bool acrThrowsException = false)
+ {
+ var commonOptions = Options.Create(new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = enableDynamicInstall,
+ EnableExternalSdkProvider = enableExternalSdkProvider,
+ EnableExternalAcrSdkProvider = enableExternalAcrSdkProvider,
+ EnableAcrSdkProvider = enableAcrSdkProvider,
+ });
+
+ var onDiskProvider = new TestPhpOnDiskVersionProvider();
+ var storageProvider = new TestPhpSdkStorageVersionProvider(
+ commonOptions,
+ new TestHttpClientFactory(),
+ NullLoggerFactory.Instance);
+ var externalProvider = new TestPhpExternalVersionProvider(
+ commonOptions,
+ new TestExternalSdkProvider(),
+ NullLoggerFactory.Instance,
+ returnsNull: externalSdkReturnsNull);
+ var externalAcrProvider = new TestPhpExternalAcrVersionProvider(
+ commonOptions,
+ NullLoggerFactory.Instance,
+ new DefaultStandardOutputWriter(),
+ returnsNull: externalAcrReturnsNull);
+ var acrProvider = new TestPhpAcrVersionProvider(
+ commonOptions,
+ new OciRegistryClient("https://test.azurecr.io", new TestHttpClientFactory(), NullLoggerFactory.Instance),
+ NullLoggerFactory.Instance,
+ throwsException: acrThrowsException);
+ var versionProvider = new PhpVersionProvider(
+ commonOptions,
+ onDiskProvider,
+ storageProvider,
+ externalProvider,
+ externalAcrProvider,
+ acrProvider,
+ NullLogger.Instance,
+ new DefaultStandardOutputWriter());
+
+ return new VersionProviderResult
+ {
+ VersionProvider = versionProvider,
+ OnDiskVersionProvider = onDiskProvider,
+ StorageVersionProvider = storageProvider,
+ ExternalVersionProvider = externalProvider,
+ ExternalAcrVersionProvider = externalAcrProvider,
+ AcrVersionProvider = acrProvider,
+ };
+ }
}
}
diff --git a/tests/BuildScriptGenerator.Tests/PlatformsInstallationScriptProviderTest.cs b/tests/BuildScriptGenerator.Tests/PlatformsInstallationScriptProviderTest.cs
index 5b5e430293..22f90c27dc 100644
--- a/tests/BuildScriptGenerator.Tests/PlatformsInstallationScriptProviderTest.cs
+++ b/tests/BuildScriptGenerator.Tests/PlatformsInstallationScriptProviderTest.cs
@@ -4,6 +4,8 @@
// --------------------------------------------------------------------------------------------
using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Oryx.Detector;
using Microsoft.Oryx.Tests.Common;
@@ -146,6 +148,7 @@ private PlatformsInstallationScriptProvider CreateEnvironmentSetupScriptProvider
var platformDetector = new DefaultPlatformsInformationProvider(
platforms,
new DefaultStandardOutputWriter(),
+ NullLogger.Instance,
Options.Create(new BuildScriptGeneratorOptions()));
return new PlatformsInstallationScriptProvider(
platforms,
diff --git a/tests/BuildScriptGenerator.Tests/Python/PythonPlatformTests.cs b/tests/BuildScriptGenerator.Tests/Python/PythonPlatformTests.cs
index f068c1a5ba..b074dc2e7c 100644
--- a/tests/BuildScriptGenerator.Tests/Python/PythonPlatformTests.cs
+++ b/tests/BuildScriptGenerator.Tests/Python/PythonPlatformTests.cs
@@ -141,6 +141,231 @@ public void UsesExternalProvider_IfDynamicInstallAndExternalProviderEnabled_AndS
Assert.Equal(TestPythonPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, snippet);
}
+ [Fact]
+ public void UsesExternalAcrProvider_IfEnabled_AndSdkIsNotAlreadyInstalled()
+ {
+ // Arrange
+ var pythonScriptGeneratorOptions = new PythonScriptGeneratorOptions();
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var versionProvider = new TestPythonVersionProvider(new[] { "3.7.5", "3.8.0" }, defaultVersion: "3.7.5");
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: true);
+ var platformInstaller = new TestPythonPlatformInstaller(
+ isVersionAlreadyInstalled: false,
+ Options.Create(commonOptions),
+ NullLoggerFactory.Instance);
+ var platform = CreatePlatformWithProviders(
+ versionProvider,
+ platformInstaller,
+ commonOptions,
+ pythonScriptGeneratorOptions,
+ externalAcrSdkProvider: externalAcrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("", PythonConstants.RequirementsFileName);
+ repo.AddFile("print(1)", "bla.py");
+ var context = new BuildScriptGeneratorContext { SourceRepo = repo };
+ var detectorResult = new PythonPlatformDetectorResult
+ {
+ Platform = PythonConstants.PlatformName,
+ PlatformVersion = "3.7.5",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.Equal(TestPythonPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, snippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ }
+
+ [Fact]
+ public void UsesDirectAcrProvider_IfEnabled_AndSdkIsNotAlreadyInstalled()
+ {
+ // Arrange
+ var pythonScriptGeneratorOptions = new PythonScriptGeneratorOptions();
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var versionProvider = new TestPythonVersionProvider(new[] { "3.7.5", "3.8.0" }, defaultVersion: "3.7.5");
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var platformInstaller = new TestPythonPlatformInstaller(
+ isVersionAlreadyInstalled: false,
+ Options.Create(commonOptions),
+ NullLoggerFactory.Instance);
+ var platform = CreatePlatformWithProviders(
+ versionProvider,
+ platformInstaller,
+ commonOptions,
+ pythonScriptGeneratorOptions,
+ acrSdkProvider: acrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("", PythonConstants.RequirementsFileName);
+ repo.AddFile("print(1)", "bla.py");
+ var context = new BuildScriptGeneratorContext { SourceRepo = repo };
+ var detectorResult = new PythonPlatformDetectorResult
+ {
+ Platform = PythonConstants.PlatformName,
+ PlatformVersion = "3.7.5",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.Equal(TestPythonPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, snippet);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void FallsBackFromExternalAcrToExternalSdk_WhenExternalAcrFails()
+ {
+ // Arrange
+ var pythonScriptGeneratorOptions = new PythonScriptGeneratorOptions();
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var versionProvider = new TestPythonVersionProvider(new[] { "3.7.5", "3.8.0" }, defaultVersion: "3.7.5");
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var platformInstaller = new TestPythonPlatformInstaller(
+ isVersionAlreadyInstalled: false,
+ Options.Create(commonOptions),
+ NullLoggerFactory.Instance);
+ var platform = CreatePlatformWithProviders(
+ versionProvider,
+ platformInstaller,
+ commonOptions,
+ pythonScriptGeneratorOptions,
+ externalAcrSdkProvider: externalAcrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("", PythonConstants.RequirementsFileName);
+ repo.AddFile("print(1)", "bla.py");
+ var context = new BuildScriptGeneratorContext { SourceRepo = repo };
+ var detectorResult = new PythonPlatformDetectorResult
+ {
+ Platform = PythonConstants.PlatformName,
+ PlatformVersion = "3.7.5",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.Equal(TestPythonPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, snippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ }
+
+ [Fact]
+ public void FallsBackFromExternalAcrToDirectAcr_WhenBothExternalProvidersFail()
+ {
+ // Arrange
+ var pythonScriptGeneratorOptions = new PythonScriptGeneratorOptions();
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var versionProvider = new TestPythonVersionProvider(new[] { "3.7.5", "3.8.0" }, defaultVersion: "3.7.5");
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: true);
+ var externalSdkProvider = new TestExternalSdkProvider(requestBlobResult: false);
+ var platformInstaller = new TestPythonPlatformInstaller(
+ isVersionAlreadyInstalled: false,
+ Options.Create(commonOptions),
+ NullLoggerFactory.Instance);
+ var platform = CreatePlatformWithProviders(
+ versionProvider,
+ platformInstaller,
+ commonOptions,
+ pythonScriptGeneratorOptions,
+ externalSdkProvider: externalSdkProvider,
+ externalAcrSdkProvider: externalAcrSdkProvider,
+ acrSdkProvider: acrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("", PythonConstants.RequirementsFileName);
+ repo.AddFile("print(1)", "bla.py");
+ var context = new BuildScriptGeneratorContext { SourceRepo = repo };
+ var detectorResult = new PythonPlatformDetectorResult
+ {
+ Platform = PythonConstants.PlatformName,
+ PlatformVersion = "3.7.5",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.Equal(TestPythonPlatformInstaller.InstallerScriptWithSkipSdkBinaryDownload, snippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
+ [Fact]
+ public void FallsBackToCdn_WhenAllProvidersFail()
+ {
+ // Arrange
+ var pythonScriptGeneratorOptions = new PythonScriptGeneratorOptions();
+ var commonOptions = new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = true,
+ EnableExternalAcrSdkProvider = true,
+ EnableExternalSdkProvider = true,
+ EnableAcrSdkProvider = true,
+ DebianFlavor = OsTypes.DebianBookworm,
+ };
+ var versionProvider = new TestPythonVersionProvider(new[] { "3.7.5", "3.8.0" }, defaultVersion: "3.7.5");
+ var externalAcrSdkProvider = new TestExternalAcrSdkProvider(returnValue: false);
+ var acrSdkProvider = new TestAcrSdkProvider(returnValue: false);
+ var externalSdkProvider = new TestExternalSdkProvider(requestBlobResult: false);
+ var platformInstaller = new TestPythonPlatformInstaller(
+ isVersionAlreadyInstalled: false,
+ Options.Create(commonOptions),
+ NullLoggerFactory.Instance);
+ var platform = CreatePlatformWithProviders(
+ versionProvider,
+ platformInstaller,
+ commonOptions,
+ pythonScriptGeneratorOptions,
+ externalSdkProvider: externalSdkProvider,
+ externalAcrSdkProvider: externalAcrSdkProvider,
+ acrSdkProvider: acrSdkProvider);
+ var repo = new MemorySourceRepo();
+ repo.AddFile("", PythonConstants.RequirementsFileName);
+ repo.AddFile("print(1)", "bla.py");
+ var context = new BuildScriptGeneratorContext { SourceRepo = repo };
+ var detectorResult = new PythonPlatformDetectorResult
+ {
+ Platform = PythonConstants.PlatformName,
+ PlatformVersion = "3.7.5",
+ };
+
+ // Act
+ var snippet = platform.GetInstallerScriptSnippet(context, detectorResult);
+
+ // Assert
+ Assert.NotNull(snippet);
+ Assert.Equal(TestPythonPlatformInstaller.InstallerScript, snippet);
+ Assert.True(externalAcrSdkProvider.RequestSdkAsyncCalled);
+ Assert.True(acrSdkProvider.RequestSdkFromAcrAsyncCalled);
+ }
+
[Fact]
public void GeneratedSnippet_DoesNotHaveInstallScript_IfVersionIsAlreadyPresentOnDisk()
{
@@ -416,6 +641,33 @@ public void Detect_ReturnsExpectedVersion_BasedOnHierarchy(
Assert.Equal(PythonConstants.PlatformName, result.Platform);
Assert.Equal(expectedSdkVersion, result.PlatformVersion);
}
+
+ [Fact]
+ public void Detect_ReturnsVersionProviderDefault_WhenExternalAcrEnabled_OverridingDetectedVersion()
+ {
+ // Arrange
+ var detectedVersion = "3.8.0";
+ var versionProviderDefault = "3.10.0";
+ var commonOptions = new BuildScriptGeneratorOptions
+ {
+ EnableExternalAcrSdkProvider = true,
+ };
+ var platform = CreatePlatform(
+ supportedVersions: new[] { detectedVersion, versionProviderDefault },
+ defaultVersion: versionProviderDefault,
+ detectedVersion: detectedVersion,
+ commonOptions: commonOptions);
+ var context = CreateContext();
+
+ // Act
+ var result = platform.Detect(context);
+
+ // Assert - ExternalACR short-circuit uses version provider's DefaultVersion,
+ // overriding the detected version
+ Assert.NotNull(result);
+ Assert.Equal(PythonConstants.PlatformName, result.Platform);
+ Assert.Equal(versionProviderDefault, result.PlatformVersion);
+ }
private PythonPlatform CreatePlatform(
IPythonVersionProvider pythonVersionProvider,
IExternalSdkProvider externalSdkProvider,
@@ -433,7 +685,38 @@ private PythonPlatform CreatePlatform(
detector: null,
platformInstaller,
externalSdkProvider,
- TelemetryClientHelper.GetTelemetryClient());
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
+ }
+
+ private PythonPlatform CreatePlatformWithProviders(
+ IPythonVersionProvider pythonVersionProvider,
+ PythonPlatformInstaller platformInstaller,
+ BuildScriptGeneratorOptions commonOptions = null,
+ PythonScriptGeneratorOptions pythonScriptGeneratorOptions = null,
+ IExternalSdkProvider externalSdkProvider = null,
+ IExternalAcrSdkProvider externalAcrSdkProvider = null,
+ IAcrSdkProvider acrSdkProvider = null)
+ {
+ commonOptions = commonOptions ?? new BuildScriptGeneratorOptions();
+ pythonScriptGeneratorOptions = pythonScriptGeneratorOptions ?? new PythonScriptGeneratorOptions();
+ externalSdkProvider = externalSdkProvider ?? new TestExternalSdkProvider();
+ externalAcrSdkProvider = externalAcrSdkProvider ?? new TestExternalAcrSdkProvider();
+ acrSdkProvider = acrSdkProvider ?? new TestAcrSdkProvider();
+ return new PythonPlatform(
+ Options.Create(commonOptions),
+ Options.Create(pythonScriptGeneratorOptions),
+ pythonVersionProvider,
+ NullLogger.Instance,
+ detector: null,
+ platformInstaller,
+ externalSdkProvider,
+ externalAcrSdkProvider,
+ acrSdkProvider,
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
}
private PythonPlatform CreatePlatform(
@@ -460,7 +743,10 @@ private PythonPlatform CreatePlatform(
detector,
new PythonPlatformInstaller(Options.Create(commonOptions), NullLoggerFactory.Instance),
externalSdkProvider,
- TelemetryClientHelper.GetTelemetryClient());
+ new TestExternalAcrSdkProvider(),
+ new TestAcrSdkProvider(),
+ TelemetryClientHelper.GetTelemetryClient(),
+ new DefaultStandardOutputWriter());
}
private BuildScriptGeneratorContext CreateContext(ISourceRepo sourceRepo = null)
diff --git a/tests/BuildScriptGenerator.Tests/Python/PythonVersionProviderTest.cs b/tests/BuildScriptGenerator.Tests/Python/PythonVersionProviderTest.cs
index 800a2b8b2c..210be2207f 100644
--- a/tests/BuildScriptGenerator.Tests/Python/PythonVersionProviderTest.cs
+++ b/tests/BuildScriptGenerator.Tests/Python/PythonVersionProviderTest.cs
@@ -1,4 +1,4 @@
-// --------------------------------------------------------------------------------------------
+// --------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
@@ -7,6 +7,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+using Microsoft.Oryx.BuildScriptGenerator;
using Microsoft.Oryx.BuildScriptGenerator.Python;
using Microsoft.Oryx.Tests.Common;
using Xunit;
@@ -79,12 +80,167 @@ public void GetsVersions_UsesExternalVersionProvider_WhenExternalProviderAndDyna
Assert.False(onDiskVersionProvider.GetVersionInfoCalled);
}
+ [Fact]
+ public void GetsVersions_UsesExternalAcrProvider_WhenExternalAcrProviderAndDynamicInstallEnabled()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.OnDiskVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_UsesAcrProvider_WhenAcrProviderAndDynamicInstallEnabled()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableAcrSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.OnDiskVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_ExternalAcrTakesPriority_OverExternalSdk()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_ExternalAcrTakesPriority_OverDirectAcr()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableAcrSdkProvider: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToStorage_WhenExternalAcrReturnsNull()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ externalAcrReturnsNull: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToStorage_WhenAcrProviderThrows()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableAcrSdkProvider: true,
+ acrThrowsException: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToDirectAcr_WhenExternalAcrAndExternalSdkReturnNull()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true,
+ enableAcrSdkProvider: true,
+ externalAcrReturnsNull: true,
+ externalSdkReturnsNull: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.False(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
+ [Fact]
+ public void GetsVersions_FallsBackToCdn_WhenAllProvidersReturnNull()
+ {
+ // Arrange
+ var result = CreateVersionProviderWithAcr(
+ enableDynamicInstall: true,
+ enableExternalAcrSdkProvider: true,
+ enableExternalSdkProvider: true,
+ enableAcrSdkProvider: true,
+ externalAcrReturnsNull: true,
+ externalSdkReturnsNull: true,
+ acrThrowsException: true);
+
+ // Act
+ var versionInfo = result.VersionProvider.GetVersionInfo();
+
+ // Assert
+ Assert.True(result.ExternalAcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.ExternalVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.AcrVersionProvider.GetVersionInfoCalled);
+ Assert.True(result.StorageVersionProvider.GetVersionInfoCalled);
+ }
+
private class TestPythonExternalVersionProvider : PythonExternalVersionProvider
{
+ private readonly bool _returnsNull;
+
public TestPythonExternalVersionProvider(
- IOptions commonOptions, IExternalSdkProvider externalProvider, ILoggerFactory loggerFactory)
+ IOptions commonOptions, IExternalSdkProvider externalProvider, ILoggerFactory loggerFactory,
+ bool returnsNull = false)
: base(commonOptions, externalProvider, loggerFactory)
{
+ _returnsNull = returnsNull;
}
public bool GetVersionInfoCalled { get; private set; }
@@ -92,8 +248,13 @@ public TestPythonExternalVersionProvider(
public override PlatformVersionInfo GetVersionInfo()
{
GetVersionInfoCalled = true;
+ if (_returnsNull)
+ {
+ return null;
+ }
- return null;
+ return PlatformVersionInfo.CreateAvailableViaExternalProvider(
+ new[] { "1.0.0" }, "1.0.0");
}
}
@@ -138,7 +299,10 @@ public override PlatformVersionInfo GetVersionInfo()
onDiskProvider,
storageProvider,
externalProvider,
- NullLogger.Instance);
+ new PythonExternalAcrVersionProvider(Options.Create(new BuildScriptGeneratorOptions()), NullLoggerFactory.Instance, new DefaultStandardOutputWriter()),
+ new PythonAcrVersionProvider(commonOptions, new OciRegistryClient("https://test.azurecr.io", new TestHttpClientFactory(), NullLoggerFactory.Instance), NullLoggerFactory.Instance),
+ NullLogger.Instance,
+ new DefaultStandardOutputWriter());
return (versionProvider, onDiskProvider, storageProvider, externalProvider);
}
@@ -158,5 +322,136 @@ public override PlatformVersionInfo GetVersionInfo()
return null;
}
}
+
+ private class TestPythonExternalAcrVersionProvider : PythonExternalAcrVersionProvider
+ {
+ private readonly bool _returnsNull;
+
+ public TestPythonExternalAcrVersionProvider(
+ IOptions options,
+ ILoggerFactory loggerFactory,
+ IStandardOutputWriter outputWriter,
+ bool returnsNull = false)
+ : base(options, loggerFactory, outputWriter)
+ {
+ _returnsNull = returnsNull;
+ }
+
+ public bool GetVersionInfoCalled { get; private set; }
+
+ public override PlatformVersionInfo GetVersionInfo()
+ {
+ GetVersionInfoCalled = true;
+ if (_returnsNull)
+ {
+ return null;
+ }
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(
+ new[] { "3.10.0" }, "3.10.0");
+ }
+ }
+
+ private class TestPythonAcrVersionProvider : PythonAcrVersionProvider
+ {
+ private readonly bool _throwsException;
+
+ public TestPythonAcrVersionProvider(
+ IOptions commonOptions,
+ OciRegistryClient ociClient,
+ ILoggerFactory loggerFactory,
+ bool throwsException = false)
+ : base(commonOptions, ociClient, loggerFactory)
+ {
+ _throwsException = throwsException;
+ }
+
+ public bool GetVersionInfoCalled { get; private set; }
+
+ public override PlatformVersionInfo GetVersionInfo()
+ {
+ GetVersionInfoCalled = true;
+ if (_throwsException)
+ {
+ throw new System.Exception("ACR provider simulated failure");
+ }
+
+ return PlatformVersionInfo.CreateAvailableOnAcr(
+ new[] { "3.10.0" }, "3.10.0");
+ }
+ }
+
+ private class VersionProviderResult
+ {
+ public IPythonVersionProvider VersionProvider { get; set; }
+
+ public TestPythonOnDiskVersionProvider OnDiskVersionProvider { get; set; }
+
+ public TestPythonSdkStorageVersionProvider StorageVersionProvider { get; set; }
+
+ public TestPythonExternalVersionProvider ExternalVersionProvider { get; set; }
+
+ public TestPythonExternalAcrVersionProvider ExternalAcrVersionProvider { get; set; }
+
+ public TestPythonAcrVersionProvider AcrVersionProvider { get; set; }
+ }
+
+ private VersionProviderResult CreateVersionProviderWithAcr(
+ bool enableDynamicInstall,
+ bool enableExternalSdkProvider = false,
+ bool enableExternalAcrSdkProvider = false,
+ bool enableAcrSdkProvider = false,
+ bool externalAcrReturnsNull = false,
+ bool externalSdkReturnsNull = false,
+ bool acrThrowsException = false)
+ {
+ var commonOptions = Options.Create(new BuildScriptGeneratorOptions()
+ {
+ EnableDynamicInstall = enableDynamicInstall,
+ EnableExternalSdkProvider = enableExternalSdkProvider,
+ EnableExternalAcrSdkProvider = enableExternalAcrSdkProvider,
+ EnableAcrSdkProvider = enableAcrSdkProvider,
+ });
+
+ var onDiskProvider = new TestPythonOnDiskVersionProvider();
+ var storageProvider = new TestPythonSdkStorageVersionProvider(
+ commonOptions,
+ new TestHttpClientFactory(),
+ NullLoggerFactory.Instance);
+ var externalProvider = new TestPythonExternalVersionProvider(
+ commonOptions,
+ new TestExternalSdkProvider(),
+ NullLoggerFactory.Instance,
+ returnsNull: externalSdkReturnsNull);
+ var externalAcrProvider = new TestPythonExternalAcrVersionProvider(
+ commonOptions,
+ NullLoggerFactory.Instance,
+ new DefaultStandardOutputWriter(),
+ returnsNull: externalAcrReturnsNull);
+ var acrProvider = new TestPythonAcrVersionProvider(
+ commonOptions,
+ new OciRegistryClient("https://test.azurecr.io", new TestHttpClientFactory(), NullLoggerFactory.Instance),
+ NullLoggerFactory.Instance,
+ throwsException: acrThrowsException);
+ var versionProvider = new PythonVersionProvider(
+ commonOptions,
+ onDiskProvider,
+ storageProvider,
+ externalProvider,
+ externalAcrProvider,
+ acrProvider,
+ NullLogger.Instance,
+ new DefaultStandardOutputWriter());
+
+ return new VersionProviderResult
+ {
+ VersionProvider = versionProvider,
+ OnDiskVersionProvider = onDiskProvider,
+ StorageVersionProvider = storageProvider,
+ ExternalVersionProvider = externalProvider,
+ ExternalAcrVersionProvider = externalAcrProvider,
+ AcrVersionProvider = acrProvider,
+ };
+ }
}
}
diff --git a/tests/BuildScriptGenerator.Tests/SocketRequestHelperTest.cs b/tests/BuildScriptGenerator.Tests/SocketRequestHelperTest.cs
new file mode 100644
index 0000000000..5c7c5f9ed0
--- /dev/null
+++ b/tests/BuildScriptGenerator.Tests/SocketRequestHelperTest.cs
@@ -0,0 +1,179 @@
+// --------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+// --------------------------------------------------------------------------------------------
+
+using System;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Oryx.BuildScriptGenerator.Tests
+{
+ public class SocketRequestHelperTest
+ {
+ [Fact]
+ public async Task SendRequestAsync_ThrowsSocketException_WhenSocketDoesNotExist()
+ {
+ // Arrange — use a path that doesn't exist
+ var nonExistentSocket = Path.Combine(Path.GetTempPath(), $"oryx-test-{Guid.NewGuid():N}.socket");
+
+ // Act & Assert — should throw SocketException (no such file)
+ await Assert.ThrowsAsync