Skip to content

[dotnet] Re-pack Selenium Manager as native dependency #16048

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 37 commits into from
Jul 16, 2025

Conversation

nvborisenko
Copy link
Member

@nvborisenko nvborisenko commented Jul 13, 2025

User description

##Why##
When user installs NuGet package, it automatically copies all SM binaries to his output. We could be smarter and copy only necessary binary, for instance if the project is supposed to run on Windows only, we don't need to copy Linux/MacOS binaries. But can we be smarter than .NET SDK?

##How##
Consider SM binary as simple "native library", which is supported by NuGet. Just put all binaries to runtimes directory per target. Now .NET SDK is responsible to copy the binaries.

Example: I am building .NET Framework 4.8 console application, and I see:

bin/Debug/net4.8/
├── MyConsole.exe
└── selenium-manager.exe

Or if I publish my application for specific RID, only necessary binary is now a part of my application. Fixes #15768


PR Type

Enhancement


Description

  • Repackage Selenium Manager as native dependency using NuGet runtime folders

  • Improve binary discovery logic with multiple probing paths

  • Support single-file and AOT deployment scenarios

  • Add legacy packages.config compatibility


Changes diagram

flowchart LR
  A["Old manager/ structure"] --> B["New runtimes/ structure"]
  B --> C["Platform-specific deployment"]
  D["Enhanced binary discovery"] --> E["Multiple probing paths"]
  E --> F["Single-file/AOT support"]
Loading

Changes walkthrough 📝

Relevant files
Enhancement
1 files
SeleniumManager.cs
Enhanced binary discovery with multiple probing paths       
+75/-30 
Configuration changes
7 files
BUILD.bazel
Updated build paths for new directory structure                   
+8/-8     
Selenium.WebDriver.StrongNamed.nuspec
Changed package paths to runtimes structure                           
+5/-5     
Selenium.WebDriver.csproj
Updated packaging to use runtimes directories                       
+17/-17 
Selenium.WebDriver.nuspec
Changed package paths to runtimes structure                           
+5/-5     
BUILD.bazel
Updated asset file paths                                                                 
+3/-2     
Selenium.WebDriver.targets
New MSBuild targets for legacy support                                     
+28/-0   
Selenium.WebDriver.targets
Transitive targets import configuration                                   
+4/-0     
Miscellaneous
2 files
Selenium.WebDriver.targets
Removed old MSBuild targets file                                                 
+0/-31   
Selenium.WebDriver.Common.Tests.csproj
Removed manual Selenium Manager import                                     
+1/-7     
Documentation
1 files
README.md
Added comprehensive package documentation                               
[link]   

Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • @nvborisenko nvborisenko marked this pull request as draft July 13, 2025 20:35
    @selenium-ci selenium-ci added C-dotnet .NET Bindings B-build Includes scripting, bazel and CI integrations labels Jul 13, 2025
    @nvborisenko nvborisenko changed the title [dotnet] Consider Selenium Manager as native dependency [dotnet] Re-pack Selenium Manager as native dependency Jul 13, 2025
    Copy link
    Contributor

    qodo-merge-pro bot commented Jul 13, 2025

    PR Reviewer Guide 🔍

    (Review updated until commit 5c40a80)

    Here are some key observations to aid the review process:

    🎫 Ticket compliance analysis 🔶

    15768 - Partially compliant

    Compliant requirements:

    • Control which selenium-manager binaries are copied during build
    • Avoid copying unnecessary binaries for target OS
    • Support RuntimeIdentifier and similar MSBuild properties for platform-specific deployment
    • Reduce "bloat" from unused platform binaries

    Non-compliant requirements:

    • Provide dedicated property for controlling binary copying (empty/default = copy all for non-breaking behavior)

    Requires further human verification:

    • Verify that .NET SDK correctly handles platform-specific binary deployment in various scenarios
    • Test compatibility with different project types (.NET Framework, .NET Core, etc.)
    • Validate single-file and AOT deployment scenarios work correctly

    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🧪 No relevant tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Complex Logic

    The binary discovery logic with multiple probing paths is complex and may be fragile. The fallback mechanism and path resolution should be thoroughly tested across different deployment scenarios.

    List<string> probingPaths = [Path.Combine(baseDirectory, seleniumManagerFileName)];
    
    // Supporting .NET5+ applications deployed as self-contained applications (single file or AOT)
    var nativeDllSearchDirectories = AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES")?.ToString();
    
    if (nativeDllSearchDirectories is not null)
    {
        probingPaths.AddRange(nativeDllSearchDirectories.Split(';').Select(path => Path.Combine(path, seleniumManagerFileName)));
    }
    
    // Still falling back to the assembly directory for compatibility with .NET Framework applications
    var assemblyDirectory = Path.GetDirectoryName(typeof(SeleniumManager).Assembly.Location);
    
    if (assemblyDirectory is not null)
    {
        probingPaths.Add(Path.Combine(assemblyDirectory, seleniumManagerFileName));
    }
    
    switch (platform)
    {
        case SupportedPlatform.Windows:
            probingPaths.Add(Path.Combine(baseDirectory, "runtimes", "win", "native", seleniumManagerFileName));
            break;
        case SupportedPlatform.Linux:
            probingPaths.Add(Path.Combine(baseDirectory, "runtimes", "linux", "native", seleniumManagerFileName));
            break;
        case SupportedPlatform.MacOS:
            probingPaths.Add(Path.Combine(baseDirectory, "runtimes", "osx", "native", seleniumManagerFileName));
            break;
    }
    
    binaryFullPath = probingPaths.Find(path => File.Exists(path));
    
    if (binaryFullPath is null)
    {
        throw new FileNotFoundException(
            $"Unable to locate Selenium Manager binary in the following paths: {string.Join(", ", probingPaths)}");
    }
    Legacy Support

    The legacy packages.config support copies all platform binaries regardless of target platform, which doesn't solve the original issue for legacy projects.

    <ItemGroup Condition="'$(UsingMicrosoftNETSdk)' != 'true'">
      <Content Include="$(MSBuildThisFileDirectory)..\..\runtimes\win\native\selenium-manager.exe">
        <Link>runtimes\win\native\%(Filename)%(Extension)</Link>
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        <Visible>False</Visible>
        <Pack>false</Pack>
      </Content>
    
      <Content Include="$(MSBuildThisFileDirectory)..\..\runtimes\linux\native\selenium-manager">
        <Link>runtimes\linux\native\%(Filename)%(Extension)</Link>
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        <Visible>False</Visible>
        <Pack>false</Pack>
      </Content>
    
      <Content Include="$(MSBuildThisFileDirectory)..\..\runtimes\osx\native\selenium-manager">
        <Link>runtimes\osx\native\%(Filename)%(Extension)</Link>
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        <Visible>False</Visible>
        <Pack>false</Pack>
      </Content>
    </ItemGroup>

    Copy link
    Contributor

    qodo-merge-pro bot commented Jul 13, 2025

    PR Code Suggestions ✨

    Latest suggestions up to 5c40a80
    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    General
    Use platform-appropriate path separator
    Suggestion Impact:The suggestion was implemented with an enhancement - the code now uses Path.PathSeparator instead of hardcoded semicolon, and also adds StringSplitOptions.RemoveEmptyEntries for better handling

    code diff:

    +            probingPaths.AddRange(nativeDllSearchDirectories.Split(new char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries).Select(path => Path.Combine(path, seleniumManagerFileName)));

    The code assumes semicolon as the path separator, but this may not be correct on
    all platforms. On Unix-like systems, the path separator is typically a colon
    (:). Consider using Path.PathSeparator or detecting the platform-appropriate
    separator.

    dotnet/src/webdriver/SeleniumManager.cs [105-111]

     // Supporting .NET5+ applications deployed as self-contained applications (single file or AOT)
     var nativeDllSearchDirectories = AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES")?.ToString();
     
     if (nativeDllSearchDirectories is not null)
     {
    -    probingPaths.AddRange(nativeDllSearchDirectories.Split(';').Select(path => Path.Combine(path, seleniumManagerFileName)));
    +    probingPaths.AddRange(nativeDllSearchDirectories.Split(Path.PathSeparator).Select(path => Path.Combine(path, seleniumManagerFileName)));
     }

    [Suggestion processed]

    Suggestion importance[1-10]: 8

    __

    Why: The suggestion correctly identifies a cross-platform bug where a hardcoded Windows path separator (;) is used, and the proposed fix using Path.PathSeparator is essential for correctness on non-Windows systems.

    Medium
    Use LINQ FirstOrDefault for consistency
    Suggestion Impact:The suggestion was implemented - the code was changed from probingPaths.Find(path => File.Exists(path)) to probingPaths.FirstOrDefault(File.Exists) on line 99

    code diff:

    +        binaryFullPath = probingPaths.FirstOrDefault(File.Exists);

    The Find method returns null when no matching element is found, but the
    subsequent null check uses is null pattern. Consider using FirstOrDefault with
    LINQ for consistency with the added using System.Linq import, or keep the
    current approach but ensure consistent null checking patterns throughout the
    codebase.

    dotnet/src/webdriver/SeleniumManager.cs [134-140]

    -binaryFullPath = probingPaths.Find(path => File.Exists(path));
    +binaryFullPath = probingPaths.FirstOrDefault(File.Exists);
     
     if (binaryFullPath is null)
     {
         throw new FileNotFoundException(
             $"Unable to locate Selenium Manager binary in the following paths: {string.Join(", ", probingPaths)}");
     }

    [Suggestion processed]

    Suggestion importance[1-10]: 3

    __

    Why: The suggestion correctly proposes using FirstOrDefault for consistency with the newly added System.Linq import, but this is a minor stylistic improvement with no functional impact.

    Low
    • Update

    Previous suggestions

    ✅ Suggestions up to commit da351d4
    CategorySuggestion                                                                                                                                    Impact
    Learned
    best practice
    Add null validation for directory
    Suggestion Impact:The suggestion was implemented through a different approach. Instead of adding ArgumentNullException.ThrowIfNull(), the code was refactored to use null-conditional checks (baseDirectory is not null) before using Path.Combine(), achieving the same goal of preventing null reference exceptions

    code diff:

    +        var baseDirectory = AppContext.BaseDirectory;
    +
    +        List<string> probingPaths = [];
    +
    +        if (baseDirectory is not null)
    +        {
    +            probingPaths.Add(Path.Combine(baseDirectory, seleniumManagerFileName));
    +
    +            switch (platform)
    +            {
    +                case SupportedPlatform.Windows:
    +                    probingPaths.Add(Path.Combine(baseDirectory, "runtimes", "win", "native", seleniumManagerFileName));
    +                    break;
    +                case SupportedPlatform.Linux:
    +                    probingPaths.Add(Path.Combine(baseDirectory, "runtimes", "linux", "native", seleniumManagerFileName));
    +                    break;
    +                case SupportedPlatform.MacOS:
    +                    probingPaths.Add(Path.Combine(baseDirectory, "runtimes", "osx", "native", seleniumManagerFileName));
    +                    break;
    +            }
    +        }

    The currentDirectory variable should be validated for null before being used in
    Path.Combine() calls. Add a null check to prevent potential
    NullReferenceException if AppContext.BaseDirectory returns null.

    dotnet/src/webdriver/SeleniumManager.cs [82-89]

    +ArgumentNullException.ThrowIfNull(currentDirectory);
    +
     binaryFullPath = platform switch
     {
         SupportedPlatform.Windows => Path.Combine(currentDirectory, "runtimes", "win", "native", "selenium-manager.exe"),
         SupportedPlatform.Linux => Path.Combine(currentDirectory, "runtimes", "linux", "native", "selenium-manager"),
         SupportedPlatform.MacOS => Path.Combine(currentDirectory, "runtimes", "osx", "native", "selenium-manager"),
         _ => throw new PlatformNotSupportedException(
             $"Selenium Manager doesn't support your runtime platform: {RuntimeInformation.OSDescription}"),
     };
    Suggestion importance[1-10]: 6

    __

    Why:
    Relevant best practice - Add null checks and validation for parameters, properties, and return values before using them to prevent NullReferenceException and other runtime errors

    Low

    @nvborisenko nvborisenko marked this pull request as ready for review July 14, 2025 18:52
    @nvborisenko
    Copy link
    Member Author

    nvborisenko commented Jul 14, 2025

    I have tested:

    • Normal tests projects
    • Modern tests projects (including AOT with Microsoft.Testing.Platform)
    • Normal console app (including single file and AOT)
    • VS Extension project (like wsix deployment)
    • WebAPI (including AOT)

    @nvborisenko
    Copy link
    Member Author

    Finally I am satisfied with these changes, as step forward!

    @nvborisenko
    Copy link
    Member Author

    I even tested VS 2017 with .NET Framework 4.6.1, it works.

    It also respects RuntimeIdentifier, PublishSingleFile with IncludeNativeLibrariesForSelfExtract.

    @nvborisenko nvborisenko merged commit f6ddd3f into SeleniumHQ:trunk Jul 16, 2025
    10 checks passed
    @nvborisenko nvborisenko deleted the dotnet-sm-native branch July 17, 2025 18:20
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    B-build Includes scripting, bazel and CI integrations C-dotnet .NET Bindings Review effort 4/5
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    [🚀 Feature]: do not copy unneccesary selenium-manager binaries via Selenium.WebDriver NuGet
    3 participants