From 51c370e2259d045827ce73ec6f983027c5664594 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Fri, 27 Feb 2026 23:50:26 +0000 Subject: [PATCH 1/4] UseReactiveUI API and bump WinForms target Update MAUI docs to use the new UseReactiveUI(rxAppBuilder => ...) initializer instead of manual RxAppBuilder.CreateReactiveUIBuilder(), moving service registrations into the rxAppBuilder callback and removing explicit scheduler singleton registrations. Also bump the minimum Windows target version for Windows Forms from 10.0.17763.0 to 10.0.19041.0 in installation and platform guideline docs. --- .../docs/getting-started/installation/maui.md | 30 ++++++++----------- .../installation/windows-forms.md | 6 ++-- .../docs/guidelines/platform/windows-forms.md | 4 +-- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/reactiveui/docs/getting-started/installation/maui.md b/reactiveui/docs/getting-started/installation/maui.md index cb7125b2..b61a7ddc 100644 --- a/reactiveui/docs/getting-started/installation/maui.md +++ b/reactiveui/docs/getting-started/installation/maui.md @@ -30,23 +30,19 @@ public static class MauiProgram var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() - .ConfigureReactiveUI(); - - // Initialize ReactiveUI with RxAppBuilder - var app = RxAppBuilder.CreateReactiveUIBuilder() - .WithMaui() - .WithViewsFromAssembly(typeof(App).Assembly) - .WithRegistration(locator => - { - // Register your services - locator.RegisterLazySingleton(() => new NavigationService()); - locator.RegisterLazySingleton(() => new DataService()); - }) - .BuildApp(); - - // Optional: Register additional MAUI-specific services - builder.Services.AddSingleton(app.MainThreadScheduler); - builder.Services.AddSingleton(app.TaskpoolScheduler); + // Initialize ReactiveUI with RxAppBuilder + .UseReactiveUI(rxAppBuilder => + { + rxAppBuilder + .WithMaui() + .WithViewsFromAssembly(typeof(App).Assembly) + .WithRegistration(locator => + { + // Register your services here + locator.RegisterLazySingleton(() => new NavigationService()); + locator.RegisterLazySingleton(() => new DataService()); + }); + }); return builder.Build(); } diff --git a/reactiveui/docs/getting-started/installation/windows-forms.md b/reactiveui/docs/getting-started/installation/windows-forms.md index 027982d0..544a40a9 100644 --- a/reactiveui/docs/getting-started/installation/windows-forms.md +++ b/reactiveui/docs/getting-started/installation/windows-forms.md @@ -28,12 +28,12 @@ Install the following packages for ReactiveUI with Windows Forms: ### Framework Requirements -Ensure you are targeting at least .NET 8.0 with Windows 10.0.17763.0: +Ensure you are targeting at least .NET 8.0 with Windows 10.0.19041.0: ```xml -net8.0-windows10.0.17763.0 +net8.0-windows10.0.19041.0 -net10.0-windows10.0.17763.0 +net10.0-windows10.0.19041.0 ``` ## Getting Started with RxAppBuilder (Recommended) diff --git a/reactiveui/docs/guidelines/platform/windows-forms.md b/reactiveui/docs/guidelines/platform/windows-forms.md index 09419cc7..66e55031 100644 --- a/reactiveui/docs/guidelines/platform/windows-forms.md +++ b/reactiveui/docs/guidelines/platform/windows-forms.md @@ -3,9 +3,9 @@ Ensure that you install `ReactiveUI.WinForms` into your application. -Please ensure that you are targeting at least windows10.0.17763.0 +Please ensure that you are targeting at least windows10.0.19041.0 -i.e `net8.0-windows10.0.17763.0` in your csproj file. +i.e `net8.0-windows10.0.19041.0` in your csproj file. Your viewmodels should inherit from `ReactiveObject` From 5b90945330d2d86ee1ce20414bf505918a94111c Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Fri, 27 Feb 2026 23:50:26 +0000 Subject: [PATCH 2/4] UseReactiveUI API and bump WinForms target Update MAUI docs to use the new UseReactiveUI(rxAppBuilder => ...) initializer instead of manual RxAppBuilder.CreateReactiveUIBuilder(), moving service registrations into the rxAppBuilder callback and removing explicit scheduler singleton registrations. Also bump the minimum Windows target version for Windows Forms from 10.0.17763.0 to 10.0.19041.0 in installation and platform guideline docs. --- .../docs/getting-started/installation/maui.md | 30 ++++++++----------- .../installation/windows-forms.md | 6 ++-- .../docs/guidelines/platform/windows-forms.md | 4 +-- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/reactiveui/docs/getting-started/installation/maui.md b/reactiveui/docs/getting-started/installation/maui.md index cb7125b2..b61a7ddc 100644 --- a/reactiveui/docs/getting-started/installation/maui.md +++ b/reactiveui/docs/getting-started/installation/maui.md @@ -30,23 +30,19 @@ public static class MauiProgram var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() - .ConfigureReactiveUI(); - - // Initialize ReactiveUI with RxAppBuilder - var app = RxAppBuilder.CreateReactiveUIBuilder() - .WithMaui() - .WithViewsFromAssembly(typeof(App).Assembly) - .WithRegistration(locator => - { - // Register your services - locator.RegisterLazySingleton(() => new NavigationService()); - locator.RegisterLazySingleton(() => new DataService()); - }) - .BuildApp(); - - // Optional: Register additional MAUI-specific services - builder.Services.AddSingleton(app.MainThreadScheduler); - builder.Services.AddSingleton(app.TaskpoolScheduler); + // Initialize ReactiveUI with RxAppBuilder + .UseReactiveUI(rxAppBuilder => + { + rxAppBuilder + .WithMaui() + .WithViewsFromAssembly(typeof(App).Assembly) + .WithRegistration(locator => + { + // Register your services here + locator.RegisterLazySingleton(() => new NavigationService()); + locator.RegisterLazySingleton(() => new DataService()); + }); + }); return builder.Build(); } diff --git a/reactiveui/docs/getting-started/installation/windows-forms.md b/reactiveui/docs/getting-started/installation/windows-forms.md index 027982d0..544a40a9 100644 --- a/reactiveui/docs/getting-started/installation/windows-forms.md +++ b/reactiveui/docs/getting-started/installation/windows-forms.md @@ -28,12 +28,12 @@ Install the following packages for ReactiveUI with Windows Forms: ### Framework Requirements -Ensure you are targeting at least .NET 8.0 with Windows 10.0.17763.0: +Ensure you are targeting at least .NET 8.0 with Windows 10.0.19041.0: ```xml -net8.0-windows10.0.17763.0 +net8.0-windows10.0.19041.0 -net10.0-windows10.0.17763.0 +net10.0-windows10.0.19041.0 ``` ## Getting Started with RxAppBuilder (Recommended) diff --git a/reactiveui/docs/guidelines/platform/windows-forms.md b/reactiveui/docs/guidelines/platform/windows-forms.md index 09419cc7..66e55031 100644 --- a/reactiveui/docs/guidelines/platform/windows-forms.md +++ b/reactiveui/docs/guidelines/platform/windows-forms.md @@ -3,9 +3,9 @@ Ensure that you install `ReactiveUI.WinForms` into your application. -Please ensure that you are targeting at least windows10.0.17763.0 +Please ensure that you are targeting at least windows10.0.19041.0 -i.e `net8.0-windows10.0.17763.0` in your csproj file. +i.e `net8.0-windows10.0.19041.0` in your csproj file. Your viewmodels should inherit from `ReactiveObject` From dba75258e55ad9e2434d8d28c2f149e70fb7ca8e Mon Sep 17 00:00:00 2001 From: Glenn Watson Date: Thu, 5 Mar 2026 19:38:56 +1100 Subject: [PATCH 3/4] Move towards a nuget fetcher way to generate api docs --- .github/workflows/main.yml | 37 +- .gitignore | 7 + .nuke/build.schema.json | 15 +- build.sh | 0 build/Build.cs | 140 ++----- build/NuGetFetcher.cs | 814 +++++++++++++++++++++++++++++++++++++ build/SourceFetcher.cs | 218 ---------- build/_build.csproj | 2 +- nuget-packages.json | 45 ++ reactiveui/api/index.md | 4 +- reactiveui/docfx.json | 380 +++++++++++++++-- reactiveui/toc.yml | 8 + 12 files changed, 1252 insertions(+), 418 deletions(-) mode change 100644 => 100755 build.sh create mode 100644 build/NuGetFetcher.cs delete mode 100644 build/SourceFetcher.cs create mode 100644 nuget-packages.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c38bd546..705eefab 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,51 +8,30 @@ on: jobs: build: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Setup .NET (With cache) uses: actions/setup-dotnet@v5.2.0 with: - dotnet-version: | - 8.0.x - 9.0.x - 10.0.x - dotnet-quality: 'preview' + dotnet-version: 10.0.x + dotnet-quality: 'preview' - - name: 'Setup Java JDK 11' - uses: actions/setup-java@v5.2.0 - with: - distribution: 'microsoft' - java-version: '11' - - - name: 'Add MSBuild to PATH' - uses: microsoft/setup-msbuild@v2.0.0 - with: - vs-prerelease: true - - - name: 'Install DotNet workloads' - shell: bash - run: | - dotnet workload install android ios tvos macos maui maccatalyst - - - name: Checkout + - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - - name: 'Cache: .nuke/temp, ~/.nuget/packages' + - name: 'Cache: .nuke/temp, NuGet packages' uses: actions/cache@v5 with: path: | .nuke/temp ~/.nuget/packages - key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + reactiveui/api/cache + key: ${{ runner.os }}-${{ hashFiles('**/global.json', 'nuget-packages.json') }} - - name: 'Restore and Compile ReactiveUI Projects' - run: ./build.cmd Compile - - name: 'Build Website' - run: ./build.cmd BuildWebsite + run: ./build.sh BuildWebsite - name: 'Deploy netlify' if: ${{ github.event_name != 'pull_request' }} diff --git a/.gitignore b/.gitignore index 2078f48d..6a9b5eeb 100644 --- a/.gitignore +++ b/.gitignore @@ -349,5 +349,12 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ +reactiveui/api/lib/ +reactiveui/api/cache/ +reactiveui/api/refs/ reactiveui/api/reactiveui/ reactiveui/api/reactivemarbles/ +reactiveui/api-android/ +reactiveui/api-test/ +reactiveui/api-windows/ +reactiveui/_site/ diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index fd63df01..f844f392 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -26,8 +26,8 @@ "enum": [ "BuildWebsite", "Clean", - "Compile", - "Restore" + "FetchPackages", + "Serve" ] }, "Verbosity": { @@ -101,13 +101,10 @@ "allOf": [ { "properties": { - "Configuration": { - "type": "string", - "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", - "enum": [ - "Debug", - "Release" - ] + "Port": { + "type": "integer", + "description": "Port for the preview server (default: 8080)", + "format": "int32" } } }, diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 diff --git a/build/Build.cs b/build/Build.cs index 400af525..b9c42fca 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -1,149 +1,59 @@ using Nuke.Common; using Nuke.Common.IO; -using Nuke.Common.ProjectModel; using Nuke.Common.Tooling; -using Nuke.Common.Tools.DotNet; -using Nuke.Common.Tools.MSBuild; using ReactiveUI.Web; using System; -using System.IO; -using System.Linq; -using static Nuke.Common.Tools.DotNet.DotNetTasks; class Build : NukeBuild { - /// Support plugins are available for: - /// - JetBrains ReSharper https://nuke.build/resharper - /// - JetBrains Rider https://nuke.build/rider - /// - Microsoft VisualStudio https://nuke.build/visualstudio - /// - Microsoft VSCode https://nuke.build/vscode - - public static int Main() => Execute(x => x.Compile); - - [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] - readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; - - private static readonly string reactiveui = nameof(reactiveui); - private static readonly string akavache = nameof(akavache); - private static readonly string fusillade = nameof(fusillade); - private static readonly string punchclock = nameof(punchclock); - private static readonly string splat = nameof(splat); - private static readonly string DynamicData = nameof(DynamicData); - private static readonly string reactivemarbles = nameof(reactivemarbles); - private static readonly string extensions = nameof(extensions); - private static readonly string[] RxUIProjects = [reactiveui, akavache, fusillade, punchclock, splat, "ReactiveUI.Validation", "ReactiveUI.Avalonia", extensions, "Maui.Plugins.Popup"]; - - private AbsolutePath RxUIAPIDirectory => RootDirectory / reactiveui / "api" / reactiveui; - private AbsolutePath RxMAPIDirectory => RootDirectory / reactiveui / "api" / reactivemarbles; + public static int Main() => Execute(x => x.BuildWebsite); + private AbsolutePath ApiLibDirectory => RootDirectory / "reactiveui" / "api" / "lib"; + private AbsolutePath ApiRefsDirectory => RootDirectory / "reactiveui" / "api" / "refs"; + private AbsolutePath ApiCacheDirectory => RootDirectory / "reactiveui" / "api" / "cache"; Target Clean => _ => _ - .Before(Restore) + .Before(FetchPackages) .Executes(() => { - RxUIAPIDirectory.DeleteDirectory(); - RxMAPIDirectory.DeleteDirectory(); + ApiLibDirectory.DeleteDirectory(); + ApiRefsDirectory.DeleteDirectory(); // Install docfx ProcessTasks.StartShell("dotnet tool update -g docfx").AssertZeroExitCode(); }); - Target Restore => _ => _ + Target FetchPackages => _ => _ .DependsOn(Clean) .Executes(() => { - // Restore ReactiveUI Projects - RxUIAPIDirectory.GetSources(RootDirectory, reactiveui, RxUIProjects); - - // Restore Reactive Marbles Projects - RxMAPIDirectory.GetSources(RootDirectory, reactivemarbles, DynamicData); + NuGetFetcher.FetchPackages(RootDirectory); }); - Target Compile => _ => _ - .DependsOn(Restore) + Target BuildWebsite => _ => _ + .DependsOn(FetchPackages) + .Produces(RootDirectory / "reactiveui" / "_site") .Executes(() => { - foreach (var project in RxUIProjects) - { - try - { - var dirRx = RxUIAPIDirectory / "external" / project / $"{project}-main" / "src"; - File.Copy(RootDirectory / "global.json", dirRx / "global.json", true); - var solutionFile = dirRx / $"{project}.sln"; - - // Find any .sln or .slnx file as a fallback - var solutionFileSearch = Directory.EnumerateFiles(dirRx, "*.sln*").FirstOrDefault(); - if (File.Exists(solutionFile) == false) - { - solutionFile += "x"; - if (File.Exists(solutionFile) == false) - { - if (File.Exists(solutionFileSearch)) - { - solutionFile = solutionFileSearch; - } - else - { - SourceFetcher.LogRepositoryError(reactiveui, project, $"Solution file not found: {dirRx / $"{project}.sln(x)"}"); - continue; - } - } - } - - SourceFetcher.LogInfo($"Restoring {solutionFile}..."); - DotNetRestore(s => s.SetProjectFile(solutionFile)); - SourceFetcher.LogInfo($"Building {solutionFile}..."); - DotNetBuild(s => s - .SetProjectFile(solutionFile) - .SetConfiguration(Configuration) - .EnableNoRestore()); - SourceFetcher.LogInfo($"{project} build complete"); - } - catch (Exception ex) - { - SourceFetcher.LogRepositoryError(reactiveui, project, ex.ToString()); - } - } - try { - var dirDd = RxMAPIDirectory / "external" / DynamicData / $"{DynamicData}-main" / "src"; - File.Copy(RootDirectory / "global.json", dirDd / "global.json", true); - var solutionFile = dirDd / $"{DynamicData}.sln"; - if (File.Exists(solutionFile) == false) - { - solutionFile += "x"; - if (File.Exists(solutionFile) == false) - { - SourceFetcher.LogRepositoryError(reactivemarbles, DynamicData, $"Solution file not found: {dirDd / $"{DynamicData}.sln(x)"}"); - return; - } - } - - SourceFetcher.LogInfo($"Building {solutionFile}..."); - MSBuildTasks.MSBuild(s => s - .SetProjectFile(solutionFile) - .SetConfiguration(Configuration) - .SetRestore(true)); - SourceFetcher.LogInfo($"{DynamicData} build complete"); + NuGetFetcher.PatchDocfxJson(RootDirectory); + ProcessTasks.StartShell("docfx reactiveui/docfx.json").AssertZeroExitCode(); + NuGetFetcher.LogInfo("Web Site build complete"); } catch (Exception ex) { - SourceFetcher.LogRepositoryError(reactivemarbles, DynamicData, ex.ToString()); + NuGetFetcher.LogError(ex.ToString()); } }); - Target BuildWebsite => _ => _ - .Produces(RootDirectory / reactiveui / "_site") - .Executes(() => - { - try - { - ProcessTasks.StartShell("docfx reactiveui/docfx.json").AssertZeroExitCode(); - SourceFetcher.LogInfo("Web Site build complete"); - } - catch (Exception ex) + [Parameter("Port for the preview server (default: 8080)")] + readonly int Port = 8080; + + Target Serve => _ => _ + .DependsOn(BuildWebsite) + .Executes(() => { - SourceFetcher.LogError(ex.ToString()); - } - }); + NuGetFetcher.LogInfo($"Serving website at http://localhost:{Port}"); + ProcessTasks.StartShell($"docfx serve reactiveui/_site -p {Port}").AssertZeroExitCode(); + }); } diff --git a/build/NuGetFetcher.cs b/build/NuGetFetcher.cs new file mode 100644 index 00000000..1c244253 --- /dev/null +++ b/build/NuGetFetcher.cs @@ -0,0 +1,814 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Reflection.PortableExecutable; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Nuke.Common.IO; +using Polly; + +namespace ReactiveUI.Web; + +internal static class NuGetFetcher +{ + private static readonly object _lockConsoleObject = new(); + + private static readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private sealed class PackageConfig + { + public string[] NugetPackageOwners { get; set; } = []; + public string[] TfmPreference { get; set; } = []; + public AdditionalPackage[] AdditionalPackages { get; set; } = []; + public string[] ExcludePackages { get; set; } = []; + public ReferencePackage[] ReferencePackages { get; set; } = []; + public Dictionary TfmOverrides { get; set; } = new(); + } + + private sealed class AdditionalPackage + { + public string Id { get; set; } = ""; + public string? Version { get; set; } + } + + private sealed class ReferencePackage + { + public string Id { get; set; } = ""; + public string? Version { get; set; } + public string TargetTfm { get; set; } = ""; + public string PathPrefix { get; set; } = "ref"; + } + + public static void FetchPackages(AbsolutePath rootDirectory) + { + var manifestPath = rootDirectory / "nuget-packages.json"; + var json = File.ReadAllText(manifestPath); + var config = JsonSerializer.Deserialize(json, _jsonOptions) + ?? throw new InvalidOperationException("Failed to deserialize nuget-packages.json"); + + // DLLs go into per-TFM subdirectories under lib/ for docfx to resolve cross-package references + var libDir = rootDirectory / "reactiveui" / "api" / "lib"; + var cacheDir = rootDirectory / "reactiveui" / "api" / "cache"; + var refsDir = rootDirectory / "reactiveui" / "api" / "refs"; + + libDir.CreateDirectory(); + cacheDir.CreateDirectory(); + refsDir.CreateDirectory(); + + // Fetch reference assembly packages (support libraries for docfx resolution) + if (config.ReferencePackages.Length > 0) + FetchReferencePackages(config.ReferencePackages, refsDir, cacheDir); + + // Discover packages from NuGet owners + var discoveredIds = DiscoverAllPackages(config); + + // Merge additional packages + foreach (var additional in config.AdditionalPackages) + { + if (!discoveredIds.Any(d => string.Equals(d.Id, additional.Id, StringComparison.OrdinalIgnoreCase))) + { + discoveredIds.Add((additional.Id, additional.Version)); + } + } + + // Apply excludes + var excludeSet = new HashSet(config.ExcludePackages, StringComparer.OrdinalIgnoreCase); + discoveredIds.RemoveAll(d => excludeSet.Contains(d.Id)); + + // Build final package list with TFM overrides + var allPackages = discoveredIds.Select(d => + { + config.TfmOverrides.TryGetValue(d.Id, out var tfm); + return (d.Id, d.Version, Tfm: tfm); + }).ToArray(); + + LogInfo($"Fetching {allPackages.Length} packages..."); + FetchGroup(libDir, cacheDir, allPackages, config.TfmPreference); + + // Copy reference assemblies into each lib/ TFM directory so docfx's + // UniversalAssemblyResolver can find them (it searches the assembly's + // own directory first, before the references section which is unreliable) + CopyRefsIntoLibDirs(libDir, refsDir); + } + + private static void CopyRefsIntoLibDirs(AbsolutePath libDir, AbsolutePath refsDir) + { + if (!Directory.Exists(libDir) || !Directory.Exists(refsDir)) + return; + + var refsTfms = Directory.GetDirectories(refsDir) + .Select(d => Path.GetFileName(d)!) + .ToList(); + + foreach (var libTfmDir in Directory.GetDirectories(libDir)) + { + var libTfm = Path.GetFileName(libTfmDir)!; + var bestRef = FindBestRefsTfm(libTfm, refsTfms); + if (bestRef == null) continue; + + var refDir = Path.Combine(refsDir, bestRef); + var count = 0; + foreach (var refDll in Directory.GetFiles(refDir, "*.dll")) + { + var destPath = Path.Combine(libTfmDir, Path.GetFileName(refDll)); + if (!File.Exists(destPath)) + { + File.Copy(refDll, destPath); + count++; + } + } + if (count > 0) + LogInfo($"Copied {count} reference assemblies into lib/{libTfm} (from refs/{bestRef})"); + } + } + + private static List<(string Id, string? Version)> DiscoverAllPackages(PackageConfig config) + { + using var client = new HttpClient(); + var retryPolicy = Policy.Handle() + .WaitAndRetryAsync( + 6, + attempt => TimeSpan.FromSeconds(0.1 * Math.Pow(2, attempt))); + + var searchEndpoint = ResolveSearchEndpoint(client, retryPolicy).GetAwaiter().GetResult(); + LogInfo($"Using NuGet search endpoint: {searchEndpoint}"); + + var allIds = new List<(string Id, string? Version)>(); + + foreach (var owner in config.NugetPackageOwners) + { + var ids = DiscoverPackagesByOwner(client, retryPolicy, searchEndpoint, owner) + .GetAwaiter().GetResult(); + LogInfo($"Discovered {ids.Count} packages for owner '{owner}'"); + allIds.AddRange(ids.Select(id => (id, (string?)null))); + } + + return allIds; + } + + private static void FetchReferencePackages( + ReferencePackage[] packages, + AbsolutePath refsDir, + AbsolutePath cacheDir) + { + using var client = new HttpClient(); + var retryPolicy = Policy.Handle() + .WaitAndRetryAsync( + 6, + attempt => TimeSpan.FromSeconds(0.1 * Math.Pow(2, attempt))); + + foreach (var pkg in packages) + { + try + { + var idLower = pkg.Id.ToLowerInvariant(); + + var version = pkg.Version; + if (version == null) + { + LogInfo($"Resolving latest version for reference package {pkg.Id}..."); + version = ResolveLatestStableVersion(client, retryPolicy, idLower) + .GetAwaiter().GetResult(); + if (version == null) + { + LogError($"Could not resolve version for reference package {pkg.Id}, skipping"); + continue; + } + } + + LogInfo($"Using reference package {pkg.Id} v{version}"); + + var versionLower = version.ToLowerInvariant(); + var nupkgPath = cacheDir / $"{idLower}.{versionLower}.nupkg"; + if (!File.Exists(nupkgPath)) + { + LogInfo($"Downloading {pkg.Id} v{version}..."); + DownloadNupkg(client, retryPolicy, idLower, versionLower, nupkgPath) + .GetAwaiter().GetResult(); + } + else + { + LogInfo($"Using cached {pkg.Id} v{version}"); + } + + var tfmRefsDir = refsDir / pkg.TargetTfm; + tfmRefsDir.CreateDirectory(); + ExtractReferenceAssemblies(nupkgPath, tfmRefsDir, pkg.Id, pkg.PathPrefix); + } + catch (Exception ex) + { + LogError($"Failed to fetch reference package {pkg.Id}: {ex.Message}"); + } + } + } + + private static bool IsManagedAssembly(Stream stream) + { + try + { + using var peReader = new PEReader(stream, PEStreamOptions.LeaveOpen); + // HasMetadata passes mixed-mode assemblies (native+managed) like + // System.EnterpriseServices.Wrapper.dll — checking ILOnly ensures we + // only extract pure managed assemblies that Roslyn can reference + return peReader.HasMetadata + && peReader.PEHeaders.CorHeader != null + && peReader.PEHeaders.CorHeader.Flags.HasFlag(CorFlags.ILOnly); + } + catch + { + return false; + } + } + + private static void ExtractReferenceAssemblies( + AbsolutePath nupkgPath, + AbsolutePath tfmRefsDir, + string packageId, + string pathPrefix) + { + using var archive = ZipFile.OpenRead(nupkgPath); + var prefix = pathPrefix.TrimEnd('/') + "/"; + var count = 0; + + foreach (var entry in archive.Entries) + { + if (!entry.FullName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + continue; + if (string.IsNullOrEmpty(entry.Name)) + continue; + + var ext = Path.GetExtension(entry.Name).ToLowerInvariant(); + if (ext is ".dll") + { + using var entryStream = entry.Open(); + using var memStream = new MemoryStream(); + entryStream.CopyTo(memStream); + + // Skip native DLLs — only extract managed .NET assemblies + memStream.Position = 0; + if (!IsManagedAssembly(memStream)) + { + LogInfo($" Skipping native DLL: {entry.Name}"); + continue; + } + + memStream.Position = 0; + var destPath = Path.Combine(tfmRefsDir, entry.Name); + using var fileStream = new FileStream(destPath, FileMode.Create); + memStream.CopyTo(fileStream); + count++; + } + } + + LogInfo($"Extracted {count} reference assemblies from {packageId} ({pathPrefix})"); + } + + private static async Task ResolveSearchEndpoint( + HttpClient client, + IAsyncPolicy retryPolicy) + { + string? endpoint = null; + + await retryPolicy.ExecuteAsync(async () => + { + var response = await client.GetAsync("https://api.nuget.org/v3/index.json"); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + using var doc = await JsonDocument.ParseAsync(stream); + + foreach (var resource in doc.RootElement.GetProperty("resources").EnumerateArray()) + { + var type = resource.GetProperty("@type").GetString(); + if (type == "SearchQueryService/3.5.0") + { + endpoint = resource.GetProperty("@id").GetString(); + break; + } + } + }); + + return endpoint ?? throw new InvalidOperationException( + "Could not find SearchQueryService/3.5.0 in NuGet service index"); + } + + private static async Task> DiscoverPackagesByOwner( + HttpClient client, + IAsyncPolicy retryPolicy, + string searchEndpoint, + string owner) + { + var packageIds = new List(); + var skip = 0; + const int take = 100; + + while (true) + { + var url = $"{searchEndpoint}?q=owner:{owner}&take={take}&skip={skip}&semVerLevel=2.0.0"; + int totalHits = 0; + + await retryPolicy.ExecuteAsync(async () => + { + var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + using var doc = await JsonDocument.ParseAsync(stream); + + totalHits = doc.RootElement.GetProperty("totalHits").GetInt32(); + + foreach (var result in doc.RootElement.GetProperty("data").EnumerateArray()) + { + // Skip deprecated packages + if (result.TryGetProperty("deprecation", out _)) + continue; + + var id = result.GetProperty("id").GetString(); + if (id != null) + packageIds.Add(id); + } + }); + + skip += take; + if (skip >= totalHits) + break; + } + + return packageIds; + } + + private static void FetchGroup( + AbsolutePath libDir, + AbsolutePath cacheDir, + (string Id, string? Version, string? Tfm)[] packages, + string[] tfmPreference) + { + using var client = new HttpClient(); + using var semaphore = new SemaphoreSlim(3, 3); + + var retryPolicy = Policy.Handle() + .WaitAndRetryAsync( + 6, + attempt => TimeSpan.FromSeconds(0.1 * Math.Pow(2, attempt))); + + Task.WaitAll(packages.Select(pkg => Task.Run(async () => + { + await semaphore.WaitAsync(); + try + { + var id = pkg.Id; + var idLower = id.ToLowerInvariant(); + + // Resolve version + var version = pkg.Version; + if (version == null) + { + LogInfo($"Resolving latest version for {id}..."); + version = await ResolveLatestStableVersion(client, retryPolicy, idLower); + if (version == null) + { + LogError($"Could not resolve version for {id}"); + return; + } + } + + LogInfo($"Using {id} v{version}"); + + // Download .nupkg (with cache) + var versionLower = version.ToLowerInvariant(); + var nupkgPath = cacheDir / $"{idLower}.{versionLower}.nupkg"; + if (!File.Exists(nupkgPath)) + { + LogInfo($"Downloading {id} v{version}..."); + await DownloadNupkg(client, retryPolicy, idLower, versionLower, nupkgPath); + } + else + { + LogInfo($"Using cached {id} v{version}"); + } + + // Extract DLL + XML + ExtractAssemblies(nupkgPath, libDir, id, pkg.Tfm, tfmPreference); + LogInfo($"Extracted {id} v{version}"); + } + catch (Exception ex) + { + LogError($"Failed to process {pkg.Id}: {ex.Message}"); + } + finally + { + semaphore.Release(); + } + })).ToArray()); + } + + private static async Task ResolveLatestStableVersion( + HttpClient client, + IAsyncPolicy retryPolicy, + string idLower) + { + string? result = null; + var url = $"https://api.nuget.org/v3-flatcontainer/{idLower}/index.json"; + + await retryPolicy.ExecuteAsync(async () => + { + var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + using var doc = await JsonDocument.ParseAsync(stream); + + var versions = doc.RootElement.GetProperty("versions") + .EnumerateArray() + .Select(v => v.GetString()!) + .Where(v => !v.Contains('-')) // Exclude prerelease + .ToArray(); + + result = versions.LastOrDefault(); // Versions are sorted ascending by the API + }); + + return result; + } + + private static async Task DownloadNupkg( + HttpClient client, + IAsyncPolicy retryPolicy, + string idLower, + string versionLower, + AbsolutePath outputPath) + { + var url = $"https://api.nuget.org/v3-flatcontainer/{idLower}/{versionLower}/{idLower}.{versionLower}.nupkg"; + + await retryPolicy.ExecuteAsync(async () => + { + var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using var contentStream = await response.Content.ReadAsStreamAsync(); + using var fileStream = new FileStream(outputPath, FileMode.Create); + await contentStream.CopyToAsync(fileStream); + }); + } + + private static void ExtractAssemblies( + AbsolutePath nupkgPath, + AbsolutePath libDir, + string packageId, + string? tfmOverride, + string[] tfmPreference) + { + using var archive = ZipFile.OpenRead(nupkgPath); + + // Find all lib/ entries grouped by TFM + var libEntries = archive.Entries + .Where(e => e.FullName.StartsWith("lib/", StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrEmpty(e.Name)) + .GroupBy(e => + { + var parts = e.FullName.Split('/'); + return parts.Length >= 2 ? parts[1] : ""; + }) + .Where(g => g.Key != "") + .ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase); + + if (libEntries.Count == 0) + { + LogInfo($"No lib/ entries found in {packageId}, skipping (source generator or meta-package)"); + return; + } + + // Select TFM + var selectedTfm = SelectTfm(libEntries.Keys, tfmOverride, tfmPreference); + if (selectedTfm == null) + { + LogError($"No matching TFM for {packageId}. Available: {string.Join(", ", libEntries.Keys)}"); + return; + } + + LogInfo($" {packageId}: selected TFM '{selectedTfm}'"); + + // Extract DLL and XML files into per-TFM subdirectory + var tfmLibDir = Path.Combine(libDir, selectedTfm); + Directory.CreateDirectory(tfmLibDir); + + foreach (var entry in libEntries[selectedTfm]) + { + var ext = Path.GetExtension(entry.Name).ToLowerInvariant(); + if (ext is ".dll" or ".xml") + { + var destPath = Path.Combine(tfmLibDir, entry.Name); + entry.ExtractToFile(destPath, overwrite: true); + } + } + } + + private static string? SelectTfm( + ICollection availableTfms, + string? tfmOverride, + string[] tfmPreference) + { + // 1. Per-package override — exact match + if (tfmOverride != null) + { + var exact = availableTfms.FirstOrDefault(t => + string.Equals(t, tfmOverride, StringComparison.OrdinalIgnoreCase)); + if (exact != null) return exact; + + // Per-package override — prefix match (e.g. "net10.0-android" matches "net10.0-android35.0") + var prefix = availableTfms.FirstOrDefault(t => + t.StartsWith(tfmOverride, StringComparison.OrdinalIgnoreCase)); + if (prefix != null) return prefix; + } + + // 2. Global preference — exact match + foreach (var pref in tfmPreference) + { + var exact = availableTfms.FirstOrDefault(t => + string.Equals(t, pref, StringComparison.OrdinalIgnoreCase)); + if (exact != null) return exact; + } + + // 3. Global preference — prefix match + foreach (var pref in tfmPreference) + { + var prefix = availableTfms.FirstOrDefault(t => + t.StartsWith(pref, StringComparison.OrdinalIgnoreCase)); + if (prefix != null) return prefix; + } + + // 4. Fallback: first available + return availableTfms.FirstOrDefault(); + } + + internal static void LogInfo(string message) + { + lock (_lockConsoleObject) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("[INFO] "); + Console.ResetColor(); + Console.WriteLine(message); + } + } + + internal static void LogError(string message) + { + lock (_lockConsoleObject) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.Write("[ERROR] "); + Console.ResetColor(); + Console.WriteLine(message); + } + } + + /// + /// Dynamically patches docfx.json metadata entries based on discovered lib/ and refs/ TFM subdirectories. + /// Groups TFMs into core (non-platform) and platform-specific sections. + /// + public static void PatchDocfxJson(AbsolutePath rootDirectory) + { + var apiDir = rootDirectory / "reactiveui" / "api"; + var libDir = apiDir / "lib"; + var refsDir = apiDir / "refs"; + var docfxPath = rootDirectory / "reactiveui" / "docfx.json"; + + if (!Directory.Exists(libDir)) + { + LogInfo("No lib/ directory found, skipping docfx.json patching"); + return; + } + + // Find lib/ TFMs that contain DLLs + var libTfms = Directory.GetDirectories(libDir) + .Where(d => Directory.GetFiles(d, "*.dll").Length > 0) + .Select(d => Path.GetFileName(d)!) + .OrderBy(t => t) + .ToList(); + + if (libTfms.Count == 0) + { + LogInfo("No TFM directories with DLLs found in lib/, skipping docfx.json patching"); + return; + } + + // Find available refs/ TFMs + var refsTfms = Directory.Exists(refsDir) + ? Directory.GetDirectories(refsDir) + .Where(d => Directory.GetFiles(d, "*.dll").Length > 0) + .Select(d => Path.GetFileName(d)!) + .ToList() + : new List(); + + LogInfo($"Discovered lib/ TFMs: {string.Join(", ", libTfms)}"); + LogInfo($"Discovered refs/ TFMs: {string.Join(", ", refsTfms)}"); + + // Read existing docfx.json + var docfxJson = File.ReadAllText(docfxPath); + using var doc = JsonDocument.Parse(docfxJson); + var root = doc.RootElement; + + // Extract shared metadata settings from the first existing metadata entry + var existingMetadata = root.GetProperty("metadata")[0]; + var sharedSettings = new Dictionary(); + foreach (var prop in existingMetadata.EnumerateObject()) + { + if (prop.Name is not "src" and not "references" and not "dest") + sharedSettings[prop.Name] = prop.Value; + } + + // Build one metadata entry per lib TFM — each TFM gets its own compilation + // with only its matching refs to avoid type system conflicts between + // .NET Framework (mscorlib) and modern .NET (System.Runtime) + var metadataEntries = new List(); + var platformLabels = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var tfm in libTfms) + { + var bestRef = FindBestRefsTfm(tfm, refsTfms); + if (bestRef == null) + { + LogInfo($"No matching refs for lib/{tfm}, skipping metadata entry"); + continue; + } + + var platformLabel = GetPlatformLabel(tfm); + var dest = platformLabel != null ? $"api-{platformLabel}" : "api"; + if (platformLabel != null) + platformLabels.Add(platformLabel); + + // Only list package DLLs in src.files (not ref DLLs that were copied in + // for resolver support). Ref DLLs are in the same dir but docfx should + // only generate docs for the actual packages. + var refDir = Path.Combine(refsDir, bestRef); + var refDllNames = Directory.Exists(refDir) + ? new HashSet( + Directory.GetFiles(refDir, "*.dll").Select(f => Path.GetFileName(f)), + StringComparer.OrdinalIgnoreCase) + : new HashSet(StringComparer.OrdinalIgnoreCase); + + var libTfmDir = Path.Combine(libDir, tfm); + var packageDlls = Directory.GetFiles(libTfmDir, "*.dll") + .Select(f => Path.GetFileName(f)) + .Where(f => !refDllNames.Contains(f!)) + .OrderBy(f => f) + .ToArray(); + + if (packageDlls.Length == 0) + { + LogInfo($"No package DLLs in lib/{tfm} (only refs), skipping"); + continue; + } + + // No references section — ref DLLs are co-located in the lib/ dir + // so docfx's UniversalAssemblyResolver finds them automatically + var srcs = new[] { new { src = $"api/lib/{tfm}", files = packageDlls } }; + + metadataEntries.Add(BuildMetadataEntry(srcs, Array.Empty(), dest, sharedSettings)); + LogInfo($"Metadata entry: lib/{tfm} ({packageDlls.Length} DLLs, refs from {bestRef}) -> {dest}"); + } + + // Build new docfx.json + var buildSection = root.GetProperty("build"); + var newDocfx = new Dictionary + { + ["metadata"] = metadataEntries, + ["build"] = PatchBuildSection(buildSection, platformLabels.ToList()) + }; + + var writeOptions = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + var newJson = JsonSerializer.Serialize(newDocfx, writeOptions); + File.WriteAllText(docfxPath, newJson); + LogInfo($"Patched docfx.json with {metadataEntries.Count} metadata entries"); + } + + private static string? GetPlatformLabel(string tfm) + { + // Modern platform TFMs (net10.0-android, net10.0-ios, etc.) + if (tfm.Contains("-android", StringComparison.OrdinalIgnoreCase)) return "android"; + if (tfm.Contains("-ios", StringComparison.OrdinalIgnoreCase)) return "ios"; + if (tfm.Contains("-maccatalyst", StringComparison.OrdinalIgnoreCase)) return "maccatalyst"; + if (tfm.Contains("-windows", StringComparison.OrdinalIgnoreCase)) return "windows"; + // Legacy platform TFMs + if (tfm.StartsWith("monoandroid", StringComparison.OrdinalIgnoreCase)) return "android"; + if (tfm.StartsWith("xamarinios", StringComparison.OrdinalIgnoreCase)) return "ios"; + if (tfm.StartsWith("xamarinmac", StringComparison.OrdinalIgnoreCase)) return "maccatalyst"; + if (tfm.StartsWith("uap", StringComparison.OrdinalIgnoreCase)) return "windows"; + return null; + } + + private static string? FindBestRefsTfm(string libTfm, List refsTfms) + { + // Strip platform suffix for matching (e.g. net10.0-android -> net10.0) + var baseTfm = libTfm.Contains('-') ? libTfm.Substring(0, libTfm.IndexOf('-')) : libTfm; + + // Exact match first + var exact = refsTfms.FirstOrDefault(r => + string.Equals(r, baseTfm, StringComparison.OrdinalIgnoreCase)); + if (exact != null) return exact; + + // Prefix match (e.g. net48 matches net481 refs, net10.0 matches net10.0 refs) + var prefix = refsTfms.FirstOrDefault(r => + baseTfm.StartsWith(r, StringComparison.OrdinalIgnoreCase)); + if (prefix != null) return prefix; + + // For netstandard, use highest modern .NET refs + if (baseTfm.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase)) + { + return refsTfms + .Where(r => r.StartsWith("net", StringComparison.OrdinalIgnoreCase) + && !r.StartsWith("net4", StringComparison.OrdinalIgnoreCase) + && !r.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(r => r) + .FirstOrDefault(); + } + + // No match — legacy TFMs (monoandroid, uap) with no available refs + return null; + } + + private static Dictionary BuildMetadataEntry( + object[] srcs, + object[] refs, + string dest, + Dictionary sharedSettings) + { + var entry = new Dictionary + { + ["src"] = srcs + }; + + if (refs.Length > 0) + entry["references"] = refs; + + entry["dest"] = dest; + + foreach (var (key, value) in sharedSettings) + entry[key] = value; + + return entry; + } + + private static Dictionary PatchBuildSection( + JsonElement buildSection, + List platformLabels) + { + var result = new Dictionary(); + + foreach (var prop in buildSection.EnumerateObject()) + { + if (prop.Name == "content") + { + var contentItems = new List(); + + // Add existing content items, filtering out previously-injected platform entries + foreach (var item in prop.Value.EnumerateArray()) + { + var isInjected = false; + if (item.TryGetProperty("files", out var files) && files.GetArrayLength() > 0) + { + var firstFile = files[0].GetString() ?? ""; + if (firstFile.StartsWith("api-", StringComparison.Ordinal) + && !firstFile.StartsWith("api/", StringComparison.Ordinal)) + isInjected = true; + } + if (!isInjected) + contentItems.Add(item); + } + + // Add platform API content entries + foreach (var label in platformLabels.OrderBy(l => l)) + { + contentItems.Add(new Dictionary + { + ["files"] = new[] { $"api-{label}/**.yml", $"api-{label}/index.md" } + }); + } + + result[prop.Name] = contentItems; + } + else + { + result[prop.Name] = prop.Value; + } + } + + return result; + } +} diff --git a/build/SourceFetcher.cs b/build/SourceFetcher.cs deleted file mode 100644 index 47d37783..00000000 --- a/build/SourceFetcher.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Nuke.Common.IO; -using Polly; - -namespace ReactiveUI.Web; - -internal static class SourceFetcher -{ - private static readonly object _lockConsoleObject = new(); - private static readonly object _lockWorkloadObject = new(); - - public static void GetSources(this AbsolutePath fileSystem, AbsolutePath rootDirectory, string owner, params string[] repositories) - { - FetchGitHubZip(fileSystem, rootDirectory, owner, repositories, "external", true, true); - } - - private static void FetchGitHubZip(AbsolutePath fileSystem, AbsolutePath rootDirectory, string owner, string[] repositories, string outputFolder, bool fetchNuGet, bool useSrc) - { - var zipCache = fileSystem / "zip"; - zipCache.CreateDirectory(); - (fileSystem / outputFolder).CreateDirectory(); - - using var client = new HttpClient(); - - // Create the semaphore. - using var semaphore = new SemaphoreSlim(3, 3); - - var waitAndRetry = Policy.Handle() - .WaitAndRetryAsync( - 6, // We can also do this with WaitAndRetryForever... but chose WaitAndRetry this time. - attempt => TimeSpan.FromSeconds(0.1 * Math.Pow(2, attempt))); // Back off! 2, 4, 8, 16 etc times 1/4-second - - Task.WaitAll(repositories.Select(repository => Task.Run(async () => - { - await semaphore.WaitAsync(); - try - { - LogRepositoryInfo(owner, repository, "Downloading"); - - var url = $"https://codeload.github.com/{owner}/{repository}/zip/main"; - - var zipFilePath = zipCache / $"{owner}-{repository}.zip"; - zipFilePath.DeleteFile(); - var extractZipPath = zipCache / $"{owner}-{repository}-extract"; - var finalPath = fileSystem / outputFolder / repository; - - extractZipPath.DeleteDirectory(); - extractZipPath.CreateDirectory(); - - // Retry the following call according to the policy - await waitAndRetry.ExecuteAsync(async () => - { - var response = await client.GetAsync(url); - - if (!response.IsSuccessStatusCode) - { - LogRepositoryInfo(owner, repository, "Could not find a valid document at: " + url + " " + response.StatusCode); - throw new HttpRequestException("Could not find a valid document at: " + url, default, response.StatusCode); - } - - using (Stream contentStream = await response.Content.ReadAsStreamAsync(), - stream = new FileStream(zipFilePath, FileMode.OpenOrCreate)) - { - await contentStream.CopyToAsync(stream); - } - - LogRepositoryInfo(owner, repository, "Extracting Files"); - ZipFile.ExtractToDirectory(zipFilePath, extractZipPath, true); - - try - { - var zipInternalPath = extractZipPath / repository + "-main"; - finalPath.DeleteDirectory(); - finalPath.CreateDirectory(); - zipInternalPath.MoveToDirectory(finalPath); - } - catch (Exception ex) - { - LogRepositoryError(owner, repository, "Failed to move: " + ex); - } - - if (fetchNuGet) - { - var directory = useSrc ? finalPath / $"{repository}-main" / "src" : finalPath; - File.Copy(rootDirectory / "global.json", directory / "global.json", true); - WorkflowRestore(owner, repository, finalPath, useSrc); - FetchNuGet(owner, repository, finalPath, useSrc); - } - - zipFilePath.DeleteDirectory(); - - LogRepositoryInfo(owner, repository, "Downloaded"); - }); - } - catch (Exception ex) - { - LogRepositoryError(owner, repository, "Failed to download: " + ex); - } - finally - { - semaphore.Release(); - } - })).ToArray()); - } - - private static void FetchNuGet(string owner, string repository, AbsolutePath finalPath, bool useSrc) - { - LogRepositoryInfo(owner, repository, "Restoring Packages for "); - - var directory = useSrc ? finalPath / $"{repository}-main" / "src" : finalPath; - - // Find any .sln or .slnx file and run dotnet restore on it - var solutionFile = Directory.EnumerateFiles(directory.ToString(), "*.sln*").FirstOrDefault(); - - if (File.Exists(directory / $"{repository}.sln")) - { - RunDotNet(directory, $"restore {repository}.sln"); - } - else if (File.Exists(directory / $"{repository}.slnx")) - { - RunDotNet(directory, $"restore {repository}.slnx"); - } - else if (solutionFile != null) - { - RunDotNet(directory, $"restore {Path.GetFileName(solutionFile)}"); - } - else - { - LogRepositoryError(owner, repository, "No solution file found to restore packages."); - } - } - - private static void WorkflowRestore(string owner, string repository, AbsolutePath finalPath, bool useSrc) - { - lock (_lockWorkloadObject) - { - LogRepositoryInfo(owner, repository, "Restoring workload for "); - - var directory = useSrc ? finalPath / $"{repository}-main" / "src" : finalPath; - - // Find any .sln or .slnx file and run dotnet restore on it - var solutionFile = Directory.EnumerateFiles(directory.ToString(), "*.sln*").FirstOrDefault(); - if (File.Exists(directory / $"{repository}.sln")) - { - RunDotNet(directory, $"workload restore {repository}.sln"); - } - else if (File.Exists(directory / $"{repository}.slnx")) - { - RunDotNet(directory, $"workload restore {repository}.slnx"); - } - else if (solutionFile != null) - { - RunDotNet(directory, $"workload restore {Path.GetFileName(solutionFile)}"); - } - else - { - LogRepositoryError(owner, repository, "No solution file found to restore workloads."); - } - } - } - - private static void RunDotNet(AbsolutePath finalPath, string parameters) - { - ProcessStartInfo startInfo = new() - { - WindowStyle = ProcessWindowStyle.Hidden, - FileName = "cmd.exe", - Arguments = $"/C dotnet {parameters}", - WorkingDirectory = finalPath.ToString() - }; - Process process = new() - { - StartInfo = startInfo - }; - process.Start(); - process.WaitForExit(); - process.Dispose(); - } - - internal static void LogInfo(string message) - { - lock (_lockConsoleObject) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("[INFO] "); - Console.ResetColor(); - Console.Write($"{message}"); - Console.WriteLine(); - } - } - - internal static void LogRepositoryInfo(string owner, string repository, string message) => - LogInfo($"{message} {owner}/{repository}..."); - - internal static void LogRepositoryError(string owner, string repository, string message) - { - LogError($"{message} {owner}/{repository}..."); - } - - internal static void LogError(string message) - { - lock (_lockConsoleObject) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.Write("[ERROR] "); - Console.ResetColor(); - Console.Write(message); - Console.WriteLine(); - } - } -} diff --git a/build/_build.csproj b/build/_build.csproj index 1e67fcb3..34383da4 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -12,7 +12,7 @@ - + diff --git a/nuget-packages.json b/nuget-packages.json new file mode 100644 index 00000000..0b555581 --- /dev/null +++ b/nuget-packages.json @@ -0,0 +1,45 @@ +{ + "nugetPackageOwners": ["reactiveui", "reactivemarbles"], + "tfmPreference": [ + "net10.0", + "net9.0", + "net8.0", + "net481", + "net471", + "net462", + "netstandard2.1", + "netstandard2.0", + "net10.0-windows10.0.19041.0", + "net9.0-windows10.0.19041.0", + "net10.0-android", + "net10.0-ios", + "net10.0-maccatalyst", + "net9.0-android", + "net9.0-ios", + "net9.0-maccatalyst" + ], + "additionalPackages": [ + { "id": "System.Reactive" }, + { "id": "DynamicData" } + ], + "excludePackages": [], + "referencePackages": [ + { "id": "Microsoft.NETCore.App.Ref", "version": "10.0.0", "targetTfm": "net10.0", "pathPrefix": "ref/net10.0" }, + { "id": "Microsoft.NETCore.App.Ref", "version": "9.0.0", "targetTfm": "net9.0", "pathPrefix": "ref/net9.0" }, + { "id": "Microsoft.NETCore.App.Ref", "version": "8.0.0", "targetTfm": "net8.0", "pathPrefix": "ref/net8.0" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net461", "targetTfm": "net461", "pathPrefix": "build/.NETFramework/v4.6.1" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net462", "targetTfm": "net462", "pathPrefix": "build/.NETFramework/v4.6.2" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net471", "targetTfm": "net471", "pathPrefix": "build/.NETFramework/v4.7.1" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net472", "targetTfm": "net472", "pathPrefix": "build/.NETFramework/v4.7.2" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net48", "targetTfm": "net48", "pathPrefix": "build/.NETFramework/v4.8" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net481", "targetTfm": "net481", "pathPrefix": "build/.NETFramework/v4.8.1" } + ], + "tfmOverrides": { + "ReactiveUI.AndroidX": "net10.0-android", + "ReactiveUI.Validation.AndroidX": "net10.0-android", + "ReactiveUI.Blend": "net10.0-windows10.0.19041.0", + "ReactiveUI.Winforms": "net10.0-windows10.0.19041.0", + "ReactiveUI.WinUI": "net10.0-windows10.0.19041.0", + "ReactiveUI.Wpf": "net10.0-windows10.0.19041.0" + } +} diff --git a/reactiveui/api/index.md b/reactiveui/api/index.md index cfa91820..8788fbd6 100644 --- a/reactiveui/api/index.md +++ b/reactiveui/api/index.md @@ -1,6 +1,6 @@ # ReactiveUI API Documentation -Browse the latest API documentation generated from the current ReactiveUI source code. This section provides reference material for all major ReactiveUI packages and related projects. +Browse the latest API documentation for all major ReactiveUI packages and related projects. This section is generated from the published NuGet packages and reflects the latest stable releases. - [ReactiveUI API](api/ReactiveUI.html) - [DynamicData API](api/DynamicData.html) @@ -9,4 +9,4 @@ Browse the latest API documentation generated from the current ReactiveUI source - [Fusillade API](api/Fusillade.html) - [Punchclock API](Punchclock.html) -> The API documentation is generated from the latest source and reflects .NET 8 compatibility and current best practices. +> The API documentation is generated from the latest stable NuGet package releases and reflects current best practices. diff --git a/reactiveui/docfx.json b/reactiveui/docfx.json index 6d8fdcee..ec831f96 100644 --- a/reactiveui/docfx.json +++ b/reactiveui/docfx.json @@ -3,50 +3,330 @@ { "src": [ { + "src": "api/lib/net10.0", "files": [ - "/**/Akavache.Core/bin/*/net10.0/Akavache.dll", - "/**/Akavache.SystemTextJson/bin/*/net10.0/Akavache.SystemTextJson.dll", - "/**/Akavache.NewtonsoftJson/bin/*/net10.0/Akavache.NewtonsoftJson.dll", - "/**/Akavache.Sqlite3/bin/*/net10.0/Akavache.Sqlite3.dll", - "/**/Akavache.EncryptedSqlite3/bin/*/net10.0/Akavache.EncryptedSqlite3.dll", - "/**/Akavache.Drawing/bin/*/net10.0/Akavache.Drawing.dll", - "/**/Akavache.Settings/bin/*/net10.0/Akavache.Settings", - "/**/Fusillade/bin/*/net10.0/Fusillade.dll", - "/**/Punchclock/bin/*/netstandard2.0/Punchclock.dll", - "/**/ReactiveUI/bin/*/net10.0/ReactiveUI.dll", - "/**/ReactiveUI.AndroidX/bin/*/net10.0-android/ReactiveUI.AndroidX.dll", - "/**/ReactiveUI.Blazor/bin/*/net10.0/ReactiveUI.Blazor.dll", - "/**/ReactiveUI.Blend/bin/*/net10.0-windows10.0.19041.0/ReactiveUI.Blend.dll", - "/**/ReactiveUI.Drawing/bin/*/net10.0/ReactiveUI.Drawing.dll", - "/**/ReactiveUI.Maui/bin/*/net10.0/ReactiveUI.Maui.dll", - "/**/ReactiveUI.Testing/bin/*/net10.0/ReactiveUI.Testing.dll", - "/**/ReactiveUI.Winforms/bin/*/net10.0-windows10.0.19041.0/ReactiveUI.Winforms.dll", - "/**/ReactiveUI.WinUI/bin/*/net10.0-windows10.0.19041.0/ReactiveUI.WinUI.dll", - "/**/ReactiveUI.Wpf/bin/*/net10.0-windows10.0.19041.0/ReactiveUI.Wpf.dll", - "/**/ReactiveUI.Validation/bin/*/net10.0/ReactiveUI.Validation.dll", - "/**/ReactiveUI.Validation.AndroidX/bin/*/net10.0-android/ReactiveUI.Validation.AndroidX.dll", - "/**/ReactiveUI.Maui.Plugins.Popup/bin/*/net10.0/ReactiveUI.Maui.Plugins.Popup.dll", - "/**/Splat.Core/bin/*/net10.0/Splat.Core.dll", - "/**/Splat.Builder/bin/*/net10.0/Splat.Builder.dll", - "/**/Splat/bin/*/net10.0/Splat.dll", - "/**/Splat.AppCenter/bin/*/net10.0/Splat.AppCenter.dll", - "/**/Splat.ApplicationInsights/bin/*/net10.0/Splat.ApplicationInsights.dll", - "/**/Splat.Autofac/bin/*/net10.0/Splat.Autofac.dll", - "/**/Splat.Drawing/bin/*/net10.0/Splat.Drawing.dll", - "/**/Splat.DryIoc/bin/*/net10.0/Splat.DryIoc.dll", - "/**/Splat.Exceptionless/bin/*/net10.0/Splat.Exceptionless.dll", - "/**/Splat.Log4Net/bin/*/net10.0/Splat.Log4Net.dll", - "/**/Splat.Microsoft.Extensions.DependencyInjection/bin/*/net10.0/Splat.Microsoft.Extensions.DependencyInjection.dll", - "/**/Splat.Microsoft.Extensions.Logging/bin/*/net10.0/Splat.Microsoft.Extensions.Logging.dll", - "/**/Splat.Ninject/bin/*/net10.0/Splat.Ninject.dll", - "/**/Splat.NLog/bin/*/net10.0/Splat.NLog.dll", - "/**/Splat.Prism/bin/*/net10.0/Splat.Prism.dll", - "/**/Splat.Prism.Forms/bin/*/net10.0/Splat.Prism.Forms.dll", - "/**/Splat.Raygun/bin/*/net10.0/Splat.Raygun.dll", - "/**/Splat.Serilog/bin/*/net10.0/Splat.Serilog.dll", - "/**/Splat.SimpleInjector/bin/*/net10.0/Splat.SimpleInjector.dll", - "/**/DynamicData/bin/*/net9.0/DynamicData.dll", - "/**/ReactiveUI.Extensions/bin/*/net10.0/ReactiveUI.Extensions.dll" + "Akavache.dll", + "Akavache.Drawing.dll", + "Akavache.EncryptedSqlite3.dll", + "Akavache.NewtonsoftJson.dll", + "Akavache.Sqlite3.dll", + "Akavache.SystemTextJson.dll", + "CrissCross.Avalonia.dll", + "CrissCross.dll", + "CrissCross.MAUI.dll", + "Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite.dll", + "Extensions.Hosting.Identity.EntityFrameworkCore.SqlServer.dll", + "Extensions.Hosting.Maui.dll", + "Extensions.Hosting.Plugins.dll", + "Extensions.Hosting.ReactiveUI.Maui.dll", + "Extensions.Hosting.SingleInstance.dll", + "Fusillade.dll", + "Punchclock.dll", + "ReactiveUI.Binding.dll", + "ReactiveUI.Binding.Maui.dll", + "ReactiveUI.Binding.Reactive.dll", + "ReactiveUI.Blazor.dll", + "ReactiveUI.dll", + "ReactiveUI.Drawing.dll", + "ReactiveUI.Extensions.dll", + "ReactiveUI.Maui.dll", + "ReactiveUI.Maui.Plugins.Popup.dll", + "ReactiveUI.Testing.dll", + "ReactiveUI.Testing.Reactive.dll", + "ReactiveUI.Uno.dll", + "ReactiveUI.Validation.dll", + "Refit.dll", + "Refit.HttpClientFactory.dll", + "Refit.Newtonsoft.Json.dll", + "Refit.Xml.dll", + "Sextant.Avalonia.dll", + "Sextant.dll", + "Sextant.Maui.dll", + "Splat.AppCenter.dll", + "Splat.ApplicationInsights.dll", + "Splat.Autofac.dll", + "Splat.Builder.dll", + "Splat.Common.Test.dll", + "Splat.Core.dll", + "Splat.dll", + "Splat.Drawing.dll", + "Splat.DryIoc.dll", + "Splat.Exceptionless.dll", + "Splat.Log4Net.dll", + "Splat.Logging.dll", + "Splat.Microsoft.Extensions.DependencyInjection.dll", + "Splat.Microsoft.Extensions.Logging.dll", + "Splat.Ninject.dll", + "Splat.NLog.dll", + "Splat.Prism.dll", + "Splat.Prism.Forms.dll", + "Splat.Raygun.dll", + "Splat.Serilog.dll", + "Splat.SimpleInjector.dll" + ] + } + ], + "dest": "api", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net10.0-android36.0", + "files": [ + "ReactiveUI.AndroidX.dll", + "ReactiveUI.Validation.AndroidX.dll" + ] + } + ], + "dest": "api-android", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net10.0-windows10.0.19041", + "files": [ + "CrissCross.WinForms.dll", + "CrissCross.WPF.dll", + "CrissCross.WPF.Plot.dll", + "CrissCross.WPF.UI.dll", + "CrissCross.WPF.UI.resources.dll", + "Extensions.Hosting.ReactiveUI.WinForms.dll", + "Extensions.Hosting.ReactiveUI.WinUI.dll", + "Extensions.Hosting.ReactiveUI.Wpf.dll", + "Extensions.Hosting.WinUI.dll", + "ReactiveUI.WinUI.dll" + ] + } + ], + "dest": "api-windows", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net10.0-windows7.0", + "files": [ + "CrissCross.WPF.WebView2.dll", + "Extensions.Hosting.WinForms.dll" + ] + } + ], + "dest": "api-windows", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net462", + "files": [ + "Extensions.Hosting.MainUIThread.dll", + "Extensions.Hosting.Wpf.dll", + "ReactiveMarbles.PropertyChanged.dll" + ] + } + ], + "dest": "api", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net481", + "files": [ + "ReactiveUI.Binding.WinForms.dll", + "ReactiveUI.Binding.Wpf.dll", + "ReactiveUI.Blend.dll", + "ReactiveUI.Winforms.dll", + "ReactiveUI.Wpf.dll" + ] + } + ], + "dest": "api", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net8.0", + "files": [ + "Akavache.Settings.dll", + "ReactiveMarbles.Locator.dll", + "ReactiveMarbles.Mvvm.dll", + "ReactiveMarbles.ViewModel.Core.dll", + "ReactiveUI.Uno.WinUI.dll", + "Sextant.Plugins.Popup.dll", + "Sextant.XamForms.dll" + ] + } + ], + "dest": "api", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net8.0-android34.0", + "files": [ + "ReactiveMarbles.ViewModel.MAUI.dll" + ] + } + ], + "dest": "api-android", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net8.0-windows10.0.19041", + "files": [ + "ReactiveMarbles.ViewModel.WinForms.dll", + "ReactiveMarbles.ViewModel.Wpf.dll" + ] + } + ], + "dest": "api-windows", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net9.0", + "files": [ + "Akavache.Mobile.dll", + "CrissCross.XamForms.dll", + "DynamicData.dll", + "Splat.Avalonia.Autofac.dll", + "Splat.Avalonia.DryIoc.dll", + "Splat.Avalonia.Microsoft.Extensions.DependencyInjection.dll", + "Splat.Avalonia.Ninject.dll", + "System.Reactive.Wasm.dll" + ] + } + ], + "dest": "api", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/netstandard2.0", + "files": [ + "Cake.PinNuGetDependency.dll", + "Extensions.Hosting.PluginService.dll", + "Minimalist.Reactive.dll", + "Pharmacist.Common.dll", + "Pharmacist.Core.dll", + "ReactiveMarbles.CacheDatabase.Core.dll", + "ReactiveMarbles.CacheDatabase.EncryptedSettings.dll", + "ReactiveMarbles.CacheDatabase.EncryptedSqlite3.dll", + "ReactiveMarbles.CacheDatabase.NewtonsoftJson.dll", + "ReactiveMarbles.CacheDatabase.Settings.dll", + "ReactiveMarbles.CacheDatabase.Sqlite3.dll", + "ReactiveMarbles.CacheDatabase.SystemTextJson.dll", + "ReactiveMarbles.Command.dll", + "ReactiveMarbles.NuGet.Helpers.dll", + "ReactiveMarbles.PropertyChanged.SourceGenerator.dll", + "ReactiveMarbles.SourceGenerator.TestNuGetHelper.dll", + "ReactiveUI.Avalonia.Autofac.dll", + "ReactiveUI.Avalonia.dll", + "ReactiveUI.Avalonia.DryIoc.dll", + "ReactiveUI.Avalonia.Microsoft.Extensions.DependencyInjection.dll", + "ReactiveUI.Avalonia.Ninject.dll", + "ReactiveUI.XamForms.dll", + "SextantSample.ViewModels.dll", + "System.Reactive.dll" ] } ], @@ -100,6 +380,18 @@ "toc.yml", "*.md" ] + }, + { + "files": [ + "api-android/**.yml", + "api-android/index.md" + ] + }, + { + "files": [ + "api-windows/**.yml", + "api-windows/index.md" + ] } ], "resource": [ @@ -126,4 +418,4 @@ "changefreq": "monthly" } } -} +} \ No newline at end of file diff --git a/reactiveui/toc.yml b/reactiveui/toc.yml index 0aeb5739..e5d061b8 100644 --- a/reactiveui/toc.yml +++ b/reactiveui/toc.yml @@ -4,6 +4,14 @@ - name: Api href: api/ homepage: api/index.md +- name: Api - Android + href: api-android/ +- name: Api - iOS + href: api-ios/ +- name: Api - macOS + href: api-maccatalyst/ +- name: Api - Windows + href: api-windows/ - name: Contribute href: contribute/ homepage: contribute/index.md From a9e637a9b7a3bee439e22d0da20aa57cef425d57 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Wed, 18 Mar 2026 23:22:15 +0000 Subject: [PATCH 4/4] Refactor website build and API doc generation Add Configuration to the Nuke schema and update Build.cs to run the BuildWebsite target by default. Consolidate web/api path variables (ApiPath, ApiLibDirectory, ApiRefsDirectory, ApiCacheDirectory, DocfxConfigPath, SiteOutputPath) and replace StartShell docfx invocations with StartProcess using explicit config and working directory. Change NuGetFetcher.FetchPackages signature to accept an apiPath and write lib/cache/refs under that path. Update reactiveui/docfx.json to add/remove and reorganize DLL entries and API lib src mappings to match the new API output layout. --- .nuke/build.schema.json | 8 +++++++ build/Build.cs | 41 +++++++++++++--------------------- build/NuGetFetcher.cs | 8 +++---- reactiveui/docfx.json | 49 ++++++++++++++--------------------------- 4 files changed, 43 insertions(+), 63 deletions(-) diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index f844f392..aef44719 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -101,6 +101,14 @@ "allOf": [ { "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, "Port": { "type": "integer", "description": "Port for the preview server (default: 8080)", diff --git a/build/Build.cs b/build/Build.cs index 386d0543..79c83d95 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -6,33 +6,20 @@ class Build : NukeBuild { - /// Support plugins are available for: - /// - JetBrains ReSharper https://nuke.build/resharper - /// - JetBrains Rider https://nuke.build/rider - /// - Microsoft VisualStudio https://nuke.build/visualstudio - /// - Microsoft VSCode https://nuke.build/vscode - - public static int Main() => Execute(x => x.Compile); + public static int Main() => Execute(x => x.BuildWebsite); [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; private static readonly string reactiveui = nameof(reactiveui); - private static readonly string akavache = nameof(akavache); - private static readonly string fusillade = nameof(fusillade); - private static readonly string punchclock = nameof(punchclock); - private static readonly string splat = nameof(splat); - private static readonly string DynamicData = nameof(DynamicData); - private static readonly string reactivemarbles = nameof(reactivemarbles); - private static readonly string extensions = nameof(extensions); - private static readonly string[] RxUIProjects = [akavache, fusillade, punchclock, splat, extensions]; ////, reactiveui, "ReactiveUI.Validation", "ReactiveUI.Avalonia", "Maui.Plugins.Popup"]; - - private AbsolutePath RxUIAPIDirectory => RootDirectory / reactiveui / "api" / reactiveui; - private AbsolutePath RxMAPIDirectory => RootDirectory / reactiveui / "api" / reactivemarbles; - - private AbsolutePath ApiLibDirectory => RootDirectory / "reactiveui" / "api" / "lib"; - private AbsolutePath ApiRefsDirectory => RootDirectory / "reactiveui" / "api" / "refs"; - private AbsolutePath ApiCacheDirectory => RootDirectory / "reactiveui" / "api" / "cache"; + private static readonly string api = nameof(api); + private AbsolutePath WebRootPath => RootDirectory / reactiveui; + private AbsolutePath ApiPath => WebRootPath / api; + private AbsolutePath ApiLibDirectory => ApiPath / "lib"; + private AbsolutePath ApiRefsDirectory => ApiPath / "refs"; + private AbsolutePath ApiCacheDirectory => ApiPath / "cache"; + private AbsolutePath DocfxConfigPath => WebRootPath / "docfx.json"; + private AbsolutePath SiteOutputPath => WebRootPath / "_site"; Target Clean => _ => _ .Before(FetchPackages) @@ -48,18 +35,19 @@ class Build : NukeBuild .DependsOn(Clean) .Executes(() => { - NuGetFetcher.FetchPackages(RootDirectory); + NuGetFetcher.FetchPackages(RootDirectory, ApiPath); }); Target BuildWebsite => _ => _ .DependsOn(FetchPackages) - .Produces(RootDirectory / "reactiveui" / "_site") + .Produces(SiteOutputPath) .Executes(() => { try { NuGetFetcher.PatchDocfxJson(RootDirectory); - ProcessTasks.StartShell("docfx reactiveui/docfx.json").AssertZeroExitCode(); + ProcessTasks.StartProcess("docfx", $"\"{DocfxConfigPath}\"", workingDirectory: RootDirectory) + .AssertZeroExitCode(); NuGetFetcher.LogInfo("Web Site build complete"); } catch (Exception ex) @@ -76,6 +64,7 @@ class Build : NukeBuild .Executes(() => { NuGetFetcher.LogInfo($"Serving website at http://localhost:{Port}"); - ProcessTasks.StartShell($"docfx serve reactiveui/_site -p {Port}").AssertZeroExitCode(); + ProcessTasks.StartProcess("docfx", $"serve \"{SiteOutputPath}\" -p {Port}", workingDirectory: RootDirectory) + .AssertZeroExitCode(); }); } diff --git a/build/NuGetFetcher.cs b/build/NuGetFetcher.cs index 1c244253..9067a5cd 100644 --- a/build/NuGetFetcher.cs +++ b/build/NuGetFetcher.cs @@ -49,7 +49,7 @@ private sealed class ReferencePackage public string PathPrefix { get; set; } = "ref"; } - public static void FetchPackages(AbsolutePath rootDirectory) + public static void FetchPackages(AbsolutePath rootDirectory, AbsolutePath apiPath) { var manifestPath = rootDirectory / "nuget-packages.json"; var json = File.ReadAllText(manifestPath); @@ -57,9 +57,9 @@ public static void FetchPackages(AbsolutePath rootDirectory) ?? throw new InvalidOperationException("Failed to deserialize nuget-packages.json"); // DLLs go into per-TFM subdirectories under lib/ for docfx to resolve cross-package references - var libDir = rootDirectory / "reactiveui" / "api" / "lib"; - var cacheDir = rootDirectory / "reactiveui" / "api" / "cache"; - var refsDir = rootDirectory / "reactiveui" / "api" / "refs"; + var libDir = apiPath / "lib"; + var cacheDir = apiPath / "cache"; + var refsDir = apiPath / "refs"; libDir.CreateDirectory(); cacheDir.CreateDirectory(); diff --git a/reactiveui/docfx.json b/reactiveui/docfx.json index ec831f96..6361748f 100644 --- a/reactiveui/docfx.json +++ b/reactiveui/docfx.json @@ -12,12 +12,17 @@ "Akavache.Sqlite3.dll", "Akavache.SystemTextJson.dll", "CrissCross.Avalonia.dll", + "CrissCross.Avalonia.UI.dll", "CrissCross.dll", "CrissCross.MAUI.dll", + "DynamicData.dll", + "Extensions.Hosting.Avalonia.dll", "Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite.dll", "Extensions.Hosting.Identity.EntityFrameworkCore.SqlServer.dll", + "Extensions.Hosting.MainUIThread.dll", "Extensions.Hosting.Maui.dll", "Extensions.Hosting.Plugins.dll", + "Extensions.Hosting.ReactiveUI.Avalonia.dll", "Extensions.Hosting.ReactiveUI.Maui.dll", "Extensions.Hosting.SingleInstance.dll", "Fusillade.dll", @@ -105,14 +110,7 @@ { "src": "api/lib/net10.0-windows10.0.19041", "files": [ - "CrissCross.WinForms.dll", - "CrissCross.WPF.dll", - "CrissCross.WPF.Plot.dll", - "CrissCross.WPF.UI.dll", - "CrissCross.WPF.UI.resources.dll", - "Extensions.Hosting.ReactiveUI.WinForms.dll", "Extensions.Hosting.ReactiveUI.WinUI.dll", - "Extensions.Hosting.ReactiveUI.Wpf.dll", "Extensions.Hosting.WinUI.dll", "ReactiveUI.WinUI.dll" ] @@ -130,35 +128,11 @@ "allowCompilationErrors": false, "filter": "filter.yml" }, - { - "src": [ - { - "src": "api/lib/net10.0-windows7.0", - "files": [ - "CrissCross.WPF.WebView2.dll", - "Extensions.Hosting.WinForms.dll" - ] - } - ], - "dest": "api-windows", - "outputFormat": "mref", - "includePrivateMembers": false, - "disableGitFeatures": false, - "disableDefaultFilter": false, - "noRestore": false, - "namespaceLayout": "flattened", - "memberLayout": "samePage", - "enumSortOrder": "alphabetic", - "allowCompilationErrors": false, - "filter": "filter.yml" - }, { "src": [ { "src": "api/lib/net462", "files": [ - "Extensions.Hosting.MainUIThread.dll", - "Extensions.Hosting.Wpf.dll", "ReactiveMarbles.PropertyChanged.dll" ] } @@ -180,6 +154,17 @@ { "src": "api/lib/net481", "files": [ + "CrissCross.WinForms.dll", + "CrissCross.WPF.dll", + "CrissCross.WPF.Plot.dll", + "CrissCross.WPF.UI.dll", + "CrissCross.WPF.UI.resources.dll", + "CrissCross.WPF.WebView2.dll", + "Extensions.Hosting.PluginService.dll", + "Extensions.Hosting.ReactiveUI.WinForms.dll", + "Extensions.Hosting.ReactiveUI.Wpf.dll", + "Extensions.Hosting.WinForms.dll", + "Extensions.Hosting.Wpf.dll", "ReactiveUI.Binding.WinForms.dll", "ReactiveUI.Binding.Wpf.dll", "ReactiveUI.Blend.dll", @@ -277,7 +262,6 @@ "files": [ "Akavache.Mobile.dll", "CrissCross.XamForms.dll", - "DynamicData.dll", "Splat.Avalonia.Autofac.dll", "Splat.Avalonia.DryIoc.dll", "Splat.Avalonia.Microsoft.Extensions.DependencyInjection.dll", @@ -304,7 +288,6 @@ "src": "api/lib/netstandard2.0", "files": [ "Cake.PinNuGetDependency.dll", - "Extensions.Hosting.PluginService.dll", "Minimalist.Reactive.dll", "Pharmacist.Common.dll", "Pharmacist.Core.dll",