Skip to content

Mono.Unix dependency is declared for net462/net48 builds too, not just netstandard2.0 #1542

Open

Description

@tombogle

Summary

SIL.Core's published nuspec declares a dependency on Mono.Unix identically across all three target-framework dependency groups — .NETFramework4.6.2, .NETFramework4.8, and .NETStandard2.0 — even though the POSIX interop it provides is presumably only ever exercised on Linux via the netstandard2.0 build. From SIL.Core 17.0.0's nuspec:

<group targetFramework=".NETFramework4.6.2">
  ...
  <dependency id="Mono.Unix" version="7.1.0-final.1.21458.1" exclude="Build,Analyzers" />
  ...
</group>
<group targetFramework=".NETFramework4.8">
  ...
  <dependency id="Mono.Unix" version="7.1.0-final.1.21458.1" exclude="Build,Analyzers" />
  ...
</group>
<group targetFramework=".NETStandard2.0">
  ...
  <dependency id="Mono.Unix" version="7.1.0-final.1.21458.1" exclude="Build,Analyzers" />
  ...
</group>

Why this matters

Mono.Unix has never had a genuinely stable NuGet release — every version ever published (including the "final" ones) carries a hyphenated suffix (e.g. 7.1.0-final.1.21458.1), which NuGet's semver parsing treats as prerelease regardless of the label. Consumers restoring via classic nuget.exe install without -Prerelease (packages.config-style restore) get "Unable to resolve dependency 'Mono.Unix'" and fail outright.

Because the dependency is declared for the .NETFramework4.6.2/.NETFramework4.8 groups too, this breaks restore for Windows-only, .NET-Framework-only consumers that never touch the Linux/netstandard2.0 build at all and have no actual runtime need for POSIX interop. This is exactly what happened downstream in sillsdev/SIL.BuildTasks#88 — SIL.BuildTasks (an MSBuild-tasks-only package with net472/netstandard2.0 targets) picked up SIL.Core 17.0.0 and broke restore for Bloom, a Windows-only consumer, purely because of this transitive dependency edge.

Suggested fix

Scope the Mono.Unix PackageReference in SIL.Core's own .csproj to only the netstandard2.0 target, e.g.:

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
  <PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
</ItemGroup>

That would drop it from the .NETFramework4.6.2/.NETFramework4.8 dependency groups in the generated nuspec entirely, so any net46x/net48-only consumer stops needing to resolve it — without changing anything for netstandard2.0/Linux consumers, who would still need it (and would still need to work around its prerelease-only status some other way, which is a separate, harder problem this issue doesn't attempt to solve).

Activity

  1. rmunn commented on Sep 2, 2026

    @rmunn
    Contributor

    I concur, though I would recommend changing the TargetFramework check to netstandard2.0 or netstandard 2.1 or >= 'net5' (though you'd want to make sure that that didn't return false for net10.0, it might have to be a fairly elaborate condition). Because as long as the targeted framework is specified in the same file there's very very little chance of drift, but since the beginning of SIL.Core.csproj lists $(TargetFrameworks);netstandard2.0 then there's a chance that the TargetFrameworks list declared in Directory.Build.Props might at some point be updated to include net8.0 or net10.0 or net12.0. (Unlikely since it would require Microsoft open-sourcing the Windows.Forms API, but theoretically possible).

    Eh, on second thought checking for == 'netstandard2.0' || == 'netstandard2.1' is almost certainly good enough, no need to complicate things with checking for >= net5.0 or whatever. We're probably never going to have the Windows.Forms libraries targeting net5.0 or higher, so my theoretical concern is probably never going to happen.

  2. rmunn commented on Sep 2, 2026

    @rmunn
    Contributor

    Did an experiment with Claude Code, and it found that the suggested TargetFramework == netstandard2.0 condition wouldn't compile. SIL.Core/Threading/GlobalMutex.cs includes using Mono.Unix.Native on line 9, which is needed by LinuxGlobalMutexAdapter. And SIL.Core/IO/FileModeOverride.cs has using Mono.Unix.Native on line 5, to enable the Syscall.umask call at line 63.

    Instead, Claude suggested the following fix:

    <PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1">
      <PrivateAssets Condition="$(TargetFramework.StartsWith('net4'))">all</PrivateAssets>
    </PackageReference>

    It cautioned that this would cause any software that runs using Mono on Linux to start failing with a FileNotFoundException (at runtime) when they try to access GlobalMutex or the GlobalWritingSystemRepository class (which uses FileModeOverride on Linux, which in turn needs Mono.Unix). But it also said that any such software could fix that runtime crash by including the Mono.Unix dependency themselves, with

    <PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />

    added to their .csproj files (or using nuget install Mono.Unix -Prerelease for projects using old-style .csproj files and a packages.config file).

  3. rmunn commented on Sep 2, 2026

    @rmunn
    Contributor

    Here's Claude's suggested patch, in full, removing the using Mono.Unix.Native from the places that asked for it but didn't actually need it, and updating the README with verbose (IMHO overly-verbose, as usual for Claude-generated text) instructions for any projects that still use Mono but are trying to update to recent SIL.Core versions to fix the runtime errors they will get when attempting to use GlobalMutex or GlobalWritingSystemRepository.

    If this looks good, I can turn it into a PR. I have not yet tested it myself — I don't have Mono set up on my current computer, so that would take some effort. So I hesitate to open a PR until I have the ability to actually test it. But this might save others some time in creating a PR to fix this.

    Note that if you ignore the overly-verbose comments, the actual code changes are quite small.

    diff --git a/CHANGELOG.md b/CHANGELOG.md
    index b7a7c06d..3fb1501d 100644
    --- a/CHANGELOG.md
    +++ b/CHANGELOG.md
    @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
    
     ### Fixed
    
    +- [SIL.Core, SIL.Windows.Forms, SIL.Windows.Forms.Keyboarding] Stopped declaring a `Mono.Unix` dependency on target frameworks that do not need it at runtime. `Mono.Unix` has only ever been published with prerelease-style version suffixes, so classic `nuget.exe install` could not restore it without `-Prerelease`. `SIL.Core` now references it privately (compile-time only) for `net462`/`net48`, `SIL.Windows.Forms.Keyboarding` references it only for `net462`/`net48` (the only targets that compile its Linux code), and `SIL.Windows.Forms` no longer references it at all since it never used it.
     - [SIL.Core.Clearshare and SIL.Windows.Forms.Clearshare] Made image-metadata reading and writing thread-safe. TagLib#'s `XmpTag` keeps mutable static state (a shared `NameTable` and the `NamespacePrefixes` dictionary) with no internal locking; concurrent metadata operations could corrupt it and then make every subsequent metadata call throw "Operations that change non-concurrent collections must have exclusive access." for the life of the process. All TagLib access is now serialized behind a single process-wide lock.
     - [SIL.Windows.Forms] Fixed `Interop.WIA.dll` deployment to provide the AnyCPU build to 64-bit (x64/AnyCPU) consumers instead of the 32-bit-only build.
     - [SIL.Core.ClearShare] Fixed `MetadataCore.GetSummaryParagraph` appending the Creator line twice and conditionally appending `RightsStatement` twice when using `CustomLicenseInfo`.
    diff --git a/README.md b/README.md
    index 18dfeadd..53afc629 100644
    --- a/README.md
    +++ b/README.md
    @@ -16,6 +16,31 @@ The Palaso library adheres to [Semantic Versioning](http://semver.org/) and
    
     Every commit creates a nuget package that is available on [nuget.org](https://www.nuget.org).
    
    +### Mono.Unix on .NET Framework
    +
    +The `net462` and `net48` builds of `SIL.Core` reference `Mono.Unix` at compile time only; it is
    +not declared as a dependency of those targets in the nuget package. This is so that consumers
    +can restore `SIL.Core` with classic `nuget.exe install`, which otherwise fails because
    +`Mono.Unix` has never had a non-prerelease release (see
    +[#1542](https://github.com/sillsdev/libpalaso/issues/1542)).
    +
    +`SIL.Core` only touches `Mono.Unix` on Unix, so Windows consumers are unaffected. Consumers
    +targeting `netstandard2.0` (and therefore .NET Core / .NET 5+) are also unaffected: they still
    +get `Mono.Unix` as a normal transitive dependency.
    +
    +This does change behavior for .NET Framework code running on Unix under Mono, which we no longer
    +support. There, `new FileModeOverride()` and `GlobalMutex.Initialize()` (or
    +`InitializeAndLock()`) will now throw `FileNotFoundException: Could not load file or assembly
    +'Mono.Unix'`. Mono's own `Mono.Posix.dll` does not satisfy the reference; it has a different
    +assembly identity even though it provides the same `Mono.Unix` namespace. Projects in that
    +situation need to reference the package themselves:
    +
    +```xml
    +<PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
    +```
    +
    +or, with `packages.config`, `nuget install Mono.Unix -Prerelease`.
    +
     ## Source Code
    
     To get the source code, you'll need Git. Then from a command line, give this command:
    diff --git a/SIL.Core/Reflection/ReflectionHelper.cs b/SIL.Core/Reflection/ReflectionHelper.cs
    index 710407f6..e42a9010 100644
    --- a/SIL.Core/Reflection/ReflectionHelper.cs
    +++ b/SIL.Core/Reflection/ReflectionHelper.cs
    @@ -3,7 +3,6 @@
     using System.IO;
     using System.Linq;
     using System.Reflection;
    -using Mono.Unix.Native;
    
     namespace SIL.Reflection
     {
    diff --git a/SIL.Core/SIL.Core.csproj b/SIL.Core/SIL.Core.csproj
    index d36ded06..cdfdc2fa 100644
    --- a/SIL.Core/SIL.Core.csproj
    +++ b/SIL.Core/SIL.Core.csproj
    @@ -19,7 +19,16 @@
         <PackageReference Include="Markdig.Signed" Version="0.37.0" />
         <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
         <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
    -    <PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
    +    <!-- Mono.Unix supplies the POSIX interop used by FileModeOverride and GlobalMutex. Those
    +         code paths only run on Unix, and the .NET Framework targets are Windows-only in
    +         practice (running them on Unix would mean Mono, which we no longer support), so there
    +         the package is a compile-time-only reference. Marking it private keeps it out of the
    +         net462/net48 dependency groups of the generated nuspec: Mono.Unix has never had a
    +         stable release, and a prerelease-versioned dependency cannot be restored by classic
    +         nuget.exe without -Prerelease. See https://github.com/sillsdev/libpalaso/issues/1542 -->
    +    <PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1">
    +      <PrivateAssets Condition="$(TargetFramework.StartsWith('net4'))">all</PrivateAssets>
    +    </PackageReference>
         <PackageReference Include="TagLibSharp" Version="2.3.0" />
         <PackageReference Include="SIL.ReleaseTasks" Version="3.1.1" PrivateAssets="All" />
         <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
    diff --git a/SIL.Windows.Forms.Keyboarding/SIL.Windows.Forms.Keyboarding.csproj b/SIL.Windows.Forms.Keyboarding/SIL.Windows.Forms.Keyboarding.csproj
    index 356766f2..81e3d615 100644
    --- a/SIL.Windows.Forms.Keyboarding/SIL.Windows.Forms.Keyboarding.csproj
    +++ b/SIL.Windows.Forms.Keyboarding/SIL.Windows.Forms.Keyboarding.csproj
    @@ -27,7 +27,6 @@
         <PackageReference Include="L10NSharp" Version="10.0.0-beta0004" />
         <PackageReference Include="L10NSharp.Windows.Forms" Version="10.0.0-beta0004" />
         <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
    -    <PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
         <PackageReference Include="SIL.ReleaseTasks" Version="3.1.1" PrivateAssets="All" />
         <PackageReference Include="KeymanLegacyBundle" Version="1.0.0" />
         <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" />
    @@ -40,6 +39,11 @@
       <ItemGroup Condition=" $(DefineConstants.Contains('NETFRAMEWORK')) ">
         <Reference Include="Microsoft.VisualBasic" />
         <Reference Include="System.Windows.Forms" />
    +    <!-- Mono.Unix (Catalog) is used by XklEngine, which lives under Linux\ and so is only
    +         compiled for the .NET Framework targets. Keeping the reference here keeps it out of
    +         the net8.0-windows dependency group of the generated nuspec.
    +         See https://github.com/sillsdev/libpalaso/issues/1542 -->
    +    <PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
       </ItemGroup>
    
       <ItemGroup>
    diff --git a/SIL.Windows.Forms/Properties/AssemblyInfo.cs b/SIL.Windows.Forms/Properties/AssemblyInfo.cs
    index 2327f40d..c3aac6d6 100644
    --- a/SIL.Windows.Forms/Properties/AssemblyInfo.cs
    +++ b/SIL.Windows.Forms/Properties/AssemblyInfo.cs
    @@ -20,8 +20,6 @@
            Copyright = "Copyright © 2007-2008 Eric Scott Albright",
            Location = "./Enchant.Net.dll", LicenseUrl = "https://opensource.org/licenses/LGPL-2.1")]
    
    -[assembly: Acknowledgement("Mono.Unix", Name = "Mono.Unix", Url = "https://www.nuget.org/packages/Mono.Unix/",
    -       LicenseUrl = "https://opensource.org/licenses/MIT")]
     [assembly: Acknowledgement("taglib-sharp", Name = "TagLib#", Url = "https://github.com/mono/taglib-sharp",
            Copyright = "Copyright (C) 2005, 2007 Brian Nickel, Copyright (C) 2006 Novell, Inc., Copyright (C) 2002,2003 Scott Wheeler (Original Implementation)",
            Location = "./taglib-sharp.dll", LicenseUrl = "https://opensource.org/licenses/LGPL-2.1")]
    diff --git a/SIL.Windows.Forms/SIL.Windows.Forms.csproj b/SIL.Windows.Forms/SIL.Windows.Forms.csproj
    index 9394b629..5e0350e3 100644
    --- a/SIL.Windows.Forms/SIL.Windows.Forms.csproj
    +++ b/SIL.Windows.Forms/SIL.Windows.Forms.csproj
    @@ -21,7 +21,6 @@
         <PackageReference Include="L10NSharp.Windows.Forms" Version="10.0.0-beta0004" />
         <PackageReference Include="Markdig.Signed" Version="0.37.0" />
         <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
    -    <PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
         <PackageReference Include="TagLibSharp" Version="2.3.0" />
         <PackageReference Include="GitVersion.MsBuild" Version="5.11.1" PrivateAssets="all" />
         <PackageReference Include="SIL.ReleaseTasks" Version="3.1.1" PrivateAssets="All" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions