File: CliBootstrapTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text.Json;
using Aspire.Cli.Acquisition;
using Aspire.Cli.Agents.AspireSkills;
using Aspire.Cli.Agents.Playwright;
using Aspire.Cli.Certificates;
using Aspire.Cli.Commands;
using Aspire.Cli.Configuration;
using Aspire.Cli.Interaction;
using Aspire.Cli.Npm;
using Aspire.Cli.Tests.Acquisition;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Aspire.Cli.Utils;
using Aspire.TestUtilities;
using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Sigstore;
using Tuf;
#if DEBUG
using System.Globalization;
using Aspire.Cli.Resources;
#endif
 
namespace Aspire.Cli.Tests;
 
/// <summary>
/// Integration tests for the production registrations in
/// <see cref="Program.BuildApplicationAsync"/>.
/// </summary>
[Collection(EnvVarMutatingTestCollection.Name)]
public class CliBootstrapTests(ITestOutputHelper outputHelper)
{
    private static readonly string[] s_fixedChannels = ["stable", "staging", "daily", "local"];
 
    private static async Task<IHost> BuildHostAsync(Dictionary<string, string?>? configurationValues = null)
    {
        var loggingOptions = Program.ParseLoggingOptions([]);
        var errorWriter = new TestStartupErrorWriter();
        var logBufferContext = new ConsoleLogBufferContext();
        var (loggerFactory, fileLoggerProvider) = Program.CreateLoggerFactory([], loggingOptions, errorWriter, logBufferContext);
        var identityChannelReader = new IdentityChannelReader(typeof(Program).Assembly);
        var startupContext = new Program.CliStartupContext(loggingOptions, errorWriter, loggerFactory, fileLoggerProvider, logBufferContext, loggerFactory.CreateLogger(Program.RootLoggerName), new ConsoleCancellationManager(finalDrainBudget: Timeout.InfiniteTimeSpan), identityChannelReader);
        return await Program.BuildApplicationAsync([], startupContext, configurationValues);
    }
 
    private static string GetBakedEntryAssemblyChannel()
    {
        var entryAssembly = Assembly.GetEntryAssembly();
        Assert.NotNull(entryAssembly);
        var bakedChannel = entryAssembly
            .GetCustomAttributes<AssemblyMetadataAttribute>()
            .Single(a => string.Equals(a.Key, "AspireCliChannel", StringComparison.Ordinal))
            .Value;
        Assert.False(string.IsNullOrEmpty(bakedChannel));
        return bakedChannel!;
    }
 
    [Fact]
    public void IdentityChannelReader_OnRunningCliAssembly_ReturnsKnownChannel()
    {
        var reader = new IdentityChannelReader(typeof(Aspire.Cli.Program).Assembly);
 
        Assert.True(reader.TryReadChannel(out var channel, out _));
 
        // Test host can be built with /p:AspireCliChannel=<anything in the accepted set>;
        // assert shape, not a single literal, so this test stops being an accidental
        // regression for non-default builds (including pr-<N> when the test host is a PR build).
        Assert.True(
            s_fixedChannels.Contains(channel) || channel.StartsWith("pr-", StringComparison.Ordinal),
            $"Unexpected channel '{channel}'; expected one of stable|staging|daily|local|pr-<N>.");
    }
 
    [Fact]
    public async Task BuildApplication_RegistersIIdentityChannelReader_AsIdentityChannelReaderInstance()
    {
        // Program.BuildApplicationAsync registers IIdentityChannelReader as a singleton,
        // backed by the default IdentityChannelReader (which reads from
        // typeof(Aspire.Cli.Program).Assembly).
        using var host = await BuildHostAsync();
 
        var reader = host.Services.GetRequiredService<IIdentityChannelReader>();
 
        Assert.NotNull(reader);
        Assert.IsType<IdentityChannelReader>(reader);
    }
 
    [Fact]
    public async Task BuildApplication_PopulatesCliExecutionContextChannel_FromIdentityChannelReader()
    {
        // The CliExecutionContext factory delegate must source Channel from
        // IIdentityChannelReader.ReadChannel() rather than the constructor default.
        // Without this wiring, the entire reseed chain would write "daily" for every
        // CLI build regardless of the baked AspireCliChannel.
        using var host = await BuildHostAsync();
 
        var reader = host.Services.GetRequiredService<IIdentityChannelReader>();
        var context = host.Services.GetRequiredService<CliExecutionContext>();
 
        Assert.True(reader.TryReadChannel(out var channel, out _));
        Assert.Equal(channel, context.IdentityChannel);
    }
 
    [Fact]
    public async Task BuildApplication_SharesSigstoreVerifierAcrossAttestationClientsAndScopes()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var aspireHome = workspace.CreateDirectory("aspire-home");
        using var aspireHomeOverride = new EnvVarOverride(CliPathHelper.AspireHomeEnvironmentVariable, aspireHome.FullName);
        using var host = await BuildHostAsync();
        var verifier = host.Services.GetRequiredService<SigstoreVerifier>();
        var trustRootProvider = host.Services.GetRequiredService<ITrustRootProvider>();
 
        for (var i = 0; i < 2; i++)
        {
            using var scope = host.Services.CreateScope();
            Assert.Same(verifier, scope.ServiceProvider.GetRequiredService<SigstoreVerifier>());
            Assert.Same(trustRootProvider, scope.ServiceProvider.GetRequiredService<ITrustRootProvider>());
            var npmChecker = Assert.IsType<SigstoreNpmProvenanceChecker>(
                scope.ServiceProvider.GetRequiredService<INpmProvenanceChecker>());
            var gitHubVerifier = Assert.IsType<GitHubArtifactAttestationVerifier>(
                scope.ServiceProvider.GetRequiredService<IGitHubArtifactAttestationVerifier>());
 
            // Inspect the dependencies actually retained by the clients, rather than only
            // resolving the singleton twice. Match field types, not compiler-generated
            // closure/primary-constructor field names, without adding test-only accessors.
            var verifyBundle = GetInstanceField<SigstoreBundleVerificationHandler>(npmChecker);
            Assert.NotNull(verifyBundle.Target);
            Assert.Same(verifier, GetInstanceField<SigstoreVerifier>(verifyBundle.Target));
            Assert.Same(verifier, GetInstanceField<SigstoreVerifier>(gitHubVerifier));
        }
    }
 
    [Fact]
    public async Task BuildApplication_CreatesPrivateTufCacheUnderAspireHome_AndDisposesProvider()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var aspireHome = workspace.CreateDirectory("aspire-home");
        using var aspireHomeOverride = new EnvVarOverride(CliPathHelper.AspireHomeEnvironmentVariable, aspireHome.FullName);
        TufTrustRootProvider provider;
        using (var host = await BuildHostAsync())
        {
            var context = host.Services.GetRequiredService<CliExecutionContext>();
            Assert.Equal(Path.Combine(aspireHome.FullName, "cache"), context.CacheDirectory.FullName);
            provider = Assert.IsType<TufTrustRootProvider>(host.Services.GetRequiredService<ITrustRootProvider>());
 
            // Cache construction creates these directories without contacting the TUF service.
            var cacheDirectory = Path.Combine(context.CacheDirectory.FullName, "tuf");
            foreach (var directory in new[] { cacheDirectory, Path.Combine(cacheDirectory, "targets") })
            {
                Assert.True(Directory.Exists(directory), $"Expected TUF cache directory: {directory}");
                if (!OperatingSystem.IsWindows())
                {
                    Assert.Equal(
                        UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute,
                        File.GetUnixFileMode(directory));
                }
            }
        }
 
        // Cancellation prevents network access even if host disposal stops disposing the provider.
        await Assert.ThrowsAsync<ObjectDisposedException>(() =>
            provider.GetTrustRootAsync(new CancellationToken(canceled: true)));
    }
 
    [Fact]
    [PlatformSpecific(TestPlatforms.Windows)]
    public async Task BuildApplication_UnwritableTufCache_DoesNotPreventCommandTreeCreation()
    {
        if (!OperatingSystem.IsWindows())
        {
            return;
        }
 
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var aspireHome = workspace.CreateDirectory("aspire-home");
        var tufDirectory = Directory.CreateDirectory(Path.Combine(aspireHome.FullName, "cache", "tuf"));
        Directory.CreateDirectory(Path.Combine(tufDirectory.FullName, "targets"));
        using var identity = WindowsIdentity.GetCurrent();
        var denyFileCreation = new FileSystemAccessRule(
            identity.User!,
            FileSystemRights.CreateFiles,
            AccessControlType.Deny);
        var security = tufDirectory.GetAccessControl();
        security.AddAccessRule(denyFileCreation);
        tufDirectory.SetAccessControl(security);
 
        try
        {
            using var aspireHomeOverride = new EnvVarOverride(CliPathHelper.AspireHomeEnvironmentVariable, aspireHome.FullName);
            using var host = await BuildHostAsync();
 
            Assert.NotNull(host.Services.GetRequiredService<RootCommand>());
            Assert.IsType<TufTrustRootProvider>(host.Services.GetRequiredService<ITrustRootProvider>());
        }
        finally
        {
            security.RemoveAccessRuleSpecific(denyFileCreation);
            tufDirectory.SetAccessControl(security);
        }
    }
 
    [Fact]
    [OuterloopTest("Requires network access to the public npm registry and Sigstore TUF service")]
    public async Task BuildApplication_VerifiesLatestPlaywrightNpmProvenance_UsingConfiguredTufCache()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var aspireHome = workspace.CreateDirectory("aspire-home");
        using var aspireHomeOverride = new EnvVarOverride(CliPathHelper.AspireHomeEnvironmentVariable, aspireHome.FullName);
        using var host = await BuildHostAsync();
        using var timeout = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
        timeout.CancelAfter(TimeSpan.FromMinutes(2));
        var cancellationToken = timeout.Token;
        using var httpClient = new HttpClient();
 
        // Intentionally use the live latest release in outerloop: recorded bundles cannot
        // detect npm provenance changes that would break secure Playwright installation.
        var package = await httpClient.GetFromJsonAsync<JsonElement>(
            $"https://registry.npmjs.org/{Uri.EscapeDataString(PlaywrightCliInstaller.PackageName)}/latest",
            cancellationToken);
        Assert.Equal(PlaywrightCliInstaller.PackageName, package.GetProperty("name").GetString());
        var version = package.GetProperty("version").GetString();
        Assert.False(string.IsNullOrEmpty(version));
        outputHelper.WriteLine($"Verifying {PlaywrightCliInstaller.PackageName}@{version}");
 
        var distribution = package.GetProperty("dist");
        var tarballUrl = distribution.GetProperty("tarball").GetString();
        Assert.NotNull(tarballUrl);
        using var tarball = await httpClient.GetStreamAsync(tarballUrl, cancellationToken);
        var digest = await SHA512.HashDataAsync(tarball, cancellationToken);
        var integrity = $"sha512-{Convert.ToBase64String(digest)}";
        Assert.Equal(distribution.GetProperty("integrity").GetString(), integrity);
 
        var checker = host.Services.GetRequiredService<INpmProvenanceChecker>();
        var result = await checker.VerifyProvenanceAsync(
            PlaywrightCliInstaller.PackageName,
            version,
            PlaywrightCliInstaller.ExpectedSourceRepository,
            PlaywrightCliInstaller.ExpectedWorkflowPath,
            PlaywrightCliInstaller.ExpectedBuildType,
            refInfo => string.Equals(refInfo.Kind, "tags", StringComparison.Ordinal) &&
                       (string.Equals(refInfo.Name, version, StringComparison.Ordinal) ||
                        string.Equals(refInfo.Name, $"v{version}", StringComparison.Ordinal)),
            integrity,
            cancellationToken);
 
        Assert.True(result.IsVerified, $"Provenance verification failed for {PlaywrightCliInstaller.PackageName}@{version}: {result.Outcome}");
 
        var context = host.Services.GetRequiredService<CliExecutionContext>();
        Assert.Equal(Path.Combine(aspireHome.FullName, "cache"), context.CacheDirectory.FullName);
        var cacheDirectory = Path.Combine(context.CacheDirectory.FullName, "tuf");
        var cache = new FileSystemTufCache(cacheDirectory);
        foreach (var role in new[] { "root", "timestamp", "snapshot", "targets" })
        {
            Assert.NotEmpty(Assert.IsType<byte[]>(cache.LoadMetadata(role)));
        }
        Assert.NotEmpty(Assert.IsType<byte[]>(cache.LoadTarget("trusted_root.json")));
 
        if (!OperatingSystem.IsWindows())
        {
            foreach (var file in Directory.EnumerateFiles(cacheDirectory, "*", SearchOption.AllDirectories))
            {
                Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(file));
            }
        }
    }
 
    [Fact]
    public async Task BuildApplication_CliExecutionContextChannel_MatchesAssemblyMetadataAttribute()
    {
        // End-to-end coherence: the channel flowing through the DI container must equal the
        // value baked into the entry assembly by [AssemblyMetadata("AspireCliChannel", "...")].
        // IdentityChannelReader reads from typeof(Aspire.Cli.Program).Assembly; this test
        // reads Assembly.GetEntryAssembly() directly and the comparison works because
        // Aspire.Cli.csproj and the test csproj forward the same $(AspireCliChannel) MSBuild
        // property — keeping both assemblies in lockstep regardless of the build configuration
        // (so this test is also correct on /p:AspireCliChannel=stable or pr-<N> CI builds).
        var bakedChannel = GetBakedEntryAssemblyChannel();
 
        using var host = await BuildHostAsync();
 
        var context = host.Services.GetRequiredService<CliExecutionContext>();
 
        Assert.Equal(bakedChannel, context.IdentityChannel);
    }
 
    [Fact]
    public async Task BuildApplication_ConfigurationValuesOverrideGlobalSettingsFile()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var aspireHome = workspace.CreateDirectory("aspire-home");
        var ambientNssDbDirectory = workspace.CreateDirectory("ambient-nssdb");
        var configuredNssDbDirectory = workspace.CreateDirectory("configured-nssdb");
        var globalConfig = JsonSerializer.Serialize(new
        {
            certificates = new
            {
                nssDbPaths = $"firefox={ambientNssDbDirectory.FullName}"
            }
        });
        File.WriteAllText(Path.Combine(aspireHome.FullName, AspireConfigFile.FileName), globalConfig);
 
        using var aspireHomeOverride = new EnvVarOverride(CliPathHelper.AspireHomeEnvironmentVariable, aspireHome.FullName);
        using var host = await BuildHostAsync(new Dictionary<string, string?>
        {
            [CertificateConfiguration.NssDbPathsConfigPath] = $"firefox={configuredNssDbDirectory.FullName}"
        });
 
        var configuration = host.Services.GetRequiredService<IConfiguration>();
 
        Assert.Equal($"firefox={configuredNssDbDirectory.FullName}", configuration[CertificateConfiguration.NssDbPathsConfigPath]);
    }
 
    [Fact]
    public async Task BuildApplication_ConfiguresCertificateManagerWithNssDbPaths()
    {
        Assert.SkipUnless(OperatingSystem.IsLinux(), "NSS certificate trust is only configured on Linux.");
 
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var nssDbDirectory = workspace.CreateDirectory("nssdb");
        using var host = await BuildHostAsync(new Dictionary<string, string?>
        {
            [CertificateConfiguration.NssDbPathsConfigPath] = $"firefox={nssDbDirectory.FullName}"
        });
 
        var manager = Assert.IsType<UnixCertificateManager>(host.Services.GetRequiredService<CertificateManager>());
        var nssDb = Assert.Single(manager.GetNssDbs(workspace.WorkspaceRoot.FullName));
 
        Assert.Equal(nssDbDirectory.FullName, nssDb.Path);
    }
 
    [Fact]
    public void ParseLoggingOptions_PrInstall_UsesInstallPrefixForDefaultLogsDirectory()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var installPrefix = Path.Combine(workspace.WorkspaceRoot.FullName, "aspire-pr-test");
        var binaryPath = WriteBinaryWithSidecar(Path.Combine(installPrefix, "dogfood", "pr-17159", "bin"), InstallSourceExtensions.PrWire);
 
        var loggingOptions = Program.ParseLoggingOptions([], binaryPath);
 
        Assert.Equal(Path.Combine(installPrefix, "logs"), loggingOptions.LogsDirectory);
        Assert.Equal(loggingOptions.LogsDirectory, Path.GetDirectoryName(loggingOptions.LogFilePath));
    }
 
    [Fact]
    public void BuildCliExecutionContext_PrInstall_UsesInstallPrefixForStateDirectoriesAndKeepsIdentityChannel()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var installPrefix = Path.Combine(workspace.WorkspaceRoot.FullName, "aspire-pr-test");
        var binaryDir = Path.Combine(installPrefix, "dogfood", "pr-17159", "bin");
        var binaryPath = WriteBinaryWithSidecar(binaryDir, InstallSourceExtensions.PrWire, channel: "pr-17159");
        var logsDirectory = Path.Combine(installPrefix, "logs");
        var logFilePath = Path.Combine(logsDirectory, "aspire.log");
 
        var environment = new TestEnvironment();
        var resolver = new IdentityResolver(
            CliTestHelper.CreateSidecarReader(outputHelper),
            typeof(Program).Assembly,
            binaryDir,
            environment);
 
        var context = Program.BuildCliExecutionContext(
            debugMode: true,
            consoleLogLevel: null,
            logsDirectory: logsDirectory,
            logFilePath: logFilePath,
            identityResolver: resolver,
            processPath: binaryPath);
 
        Assert.Equal(Path.Combine(installPrefix, "hives"), context.HivesDirectory.FullName);
        Assert.Equal(Path.Combine(installPrefix, "cache"), context.CacheDirectory.FullName);
        Assert.Equal(Path.Combine(installPrefix, "sdks"), context.SdksDirectory.FullName);
        Assert.Equal(Path.Combine(installPrefix, "packages"), context.PackagesDirectory?.FullName);
        Assert.Equal(installPrefix, context.AspireHomeDirectory.FullName);
        Assert.Equal(logsDirectory, context.LogsDirectory.FullName);
        Assert.Equal(logFilePath, context.LogFilePath);
        Assert.True(context.DebugMode);
        Assert.Equal("pr-17159", context.IdentityChannel);
    }
 
    [Fact]
    public void BuildCliExecutionContext_NuGetServiceIndexOverrideFromEnv_MarksIdentityOverridden()
    {
        // Setting only ASPIRE_CLI_NUGET_SERVICE_INDEX must still flag the run as an emulation so the
        // startup override notice fires and tooling does not mistake a diagnostic run for a real build.
        // Regression guard: this source was previously omitted from the identityOverridden computation.
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var envVars = new Dictionary<string, string?> { [IdentityResolver.NuGetServiceIndexEnvVar] = "http://localhost:5000/v3/index.json" };
        var environment = new TestEnvironment(envVars);
        var resolver = new IdentityResolver(
            CliTestHelper.CreateSidecarReader(outputHelper),
            typeof(Program).Assembly,
            binaryDir: null,
            environment);
 
        var context = Program.BuildCliExecutionContext(
            debugMode: false,
            consoleLogLevel: null,
            logsDirectory: Path.Combine(workspace.WorkspaceRoot.FullName, "logs"),
            logFilePath: Path.Combine(workspace.WorkspaceRoot.FullName, "logs", "aspire.log"),
            identityResolver: resolver);
 
        Assert.True(context.IdentityOverridden);
        Assert.Equal("http://localhost:5000/v3/index.json", context.NuGetServiceIndexOverride);
    }
 
    [Fact]
    public void BuildCliExecutionContext_NoOverrides_DoesNotMarkIdentityOverridden()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var environment = new TestEnvironment();
        var resolver = new IdentityResolver(
            CliTestHelper.CreateSidecarReader(outputHelper),
            typeof(Program).Assembly,
            binaryDir: null,
            environment);
 
        var context = Program.BuildCliExecutionContext(
            debugMode: false,
            consoleLogLevel: null,
            logsDirectory: Path.Combine(workspace.WorkspaceRoot.FullName, "logs"),
            logFilePath: Path.Combine(workspace.WorkspaceRoot.FullName, "logs", "aspire.log"),
            identityResolver: resolver);
 
        Assert.False(context.IdentityOverridden);
        Assert.Null(context.NuGetServiceIndexOverride);
    }
 
#if DEBUG
    [Theory]
    [InlineData("ls --cli-wait-for-debugger")]
    [InlineData("run --cli-wait-for-debugger")]
    [InlineData("doctor --cli-wait-for-debugger")]
    public void WaitForDebuggerIfRequested_WithSubcommand_CallsShowStatus(string commandLine)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var testInteractionService = new TestInteractionService();
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => testInteractionService;
        });
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var parseResult = command.Parse(commandLine);
 
        var waitActionCalled = false;
        Program.WaitForDebuggerIfRequested(parseResult, provider, waitAction: () => waitActionCalled = true);
 
        Assert.True(waitActionCalled);
        var expectedStatus = string.Format(CultureInfo.CurrentCulture, RootCommandStrings.WaitingForDebugger, Environment.ProcessId);
        Assert.Collection(testInteractionService.ShownStatuses, status => Assert.Equal(expectedStatus, status));
    }
 
    [Fact]
    public void WaitForDebuggerIfRequested_WithoutFlag_DoesNotCallShowStatus()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var testInteractionService = new TestInteractionService();
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => testInteractionService;
        });
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var parseResult = command.Parse("ls");
 
        var waitActionCalled = false;
        Program.WaitForDebuggerIfRequested(parseResult, provider, waitAction: () => waitActionCalled = true);
 
        Assert.False(waitActionCalled);
        Assert.Empty(testInteractionService.ShownStatuses);
    }
#endif
 
    private static T GetInstanceField<T>(object instance)
    {
        var field = Assert.Single(instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic),
            field => field.FieldType == typeof(T));
 
        return Assert.IsType<T>(field.GetValue(instance));
    }
 
    private static string WriteBinaryWithSidecar(string binaryDir, string source, string? channel = null)
    {
        Directory.CreateDirectory(binaryDir);
        var binaryPath = Path.Combine(binaryDir, OperatingSystem.IsWindows() ? "aspire.exe" : "aspire");
        File.WriteAllText(binaryPath, string.Empty);
        var channelField = channel is not null ? $",\"channel\":\"{channel}\"" : "";
        File.WriteAllText(Path.Combine(binaryDir, InstallSidecarReader.SidecarFileName), $$"""{"source":"{{source}}"{{channelField}}}""");
 
        return binaryPath;
    }
}