feat: Adds Build Tool for Publishing/Setup (#2392)
## Summary Replaces the basic `publish.cmd`/`publish.sh` scripts with an interactive **BuildTool** — a C# console app using [Spectre.Console](https://spectreconsole.net/) that guides users through publishing, prerequisite checking, and cross-compilation. ### Why The community found the existing publish scripts unhelpful for newcomers. They worked but didn't walk users through the process, didn't check prerequisites, and provided no feedback when things went wrong. ### What's New **Interactive BuildTool** (`Projects/BuildTool/`) - NativeAOT-compiled C# console app with true-color ASCII logo and ModernUO brand gold/silver palette - Guided publish wizard with step-by-step back navigation (Ctrl+C or menu "Back" to go to previous step) - Prerequisite checking: .NET SDK version, VC++ Redistributable (Windows), native libraries (Linux/macOS) - .NET SDK auto-install offer via Microsoft's official install scripts - Platform detection: Windows 10 vs 11 (build number), macOS codenames, Linux distro + kernel version - Cross-compilation support: skips native library checks, shows target prerequisites after build - Non-interactive mode for CI: `--config Release --skip-prereqs` - Backward-compatible positional args: `publish.cmd release win x64` still works **Shell Wrappers** (`publish.cmd`, `publish.ps1`, `publish.sh`) - Try native BuildTool binary first (downloaded from GitHub Releases) - Fall back to `dotnet run --project Projects/BuildTool` if unavailable - SDK bootstrapping: offer to install .NET if not found **CI/CD Updates** - Build/test workflows target `Projects/Application/Application.csproj` instead of the solution (excludes BuildTool and test projects from publish) - New `build-tool-release.yml` workflow builds NativeAOT binaries for win-x64, win-arm64, osx-arm64, linux-x64, linux-arm64 - Minimum SDK bumped to 10.0.201 (required for Serialization Generator 2.14.3 / Roslyn 5.3.0) **Other Changes** - Solution converted from `.sln` to `.slnx` - Updated README with interactive mode instructions and deployment guidance ## Screenshots <img width="320" height="378" alt="image" src="https://github.com/user-attachments/assets/83c057c5-3992-4dbd-99fa-0e3c24ef6428" /> <img width="749" height="554" alt="image" src="https://github.com/user-attachments/assets/e4ee4d6f-71d4-47f9-86b6-8fd1ca3c3e7a" />
This commit is contained in:
parent
3b4e1137f6
commit
ec4d6a7a85
32 changed files with 2372 additions and 165 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"modernuoschemagenerator": {
|
"modernuoschemagenerator": {
|
||||||
"version": "2.14.2",
|
"version": "2.14.3",
|
||||||
"commands": [
|
"commands": [
|
||||||
"ModernUOSchemaGenerator"
|
"ModernUOSchemaGenerator"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
8
.github/workflows/build-test.yml
vendored
8
.github/workflows/build-test.yml
vendored
|
|
@ -14,10 +14,10 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: macos-14
|
|
||||||
name: MacOS 14
|
|
||||||
- os: macos-15
|
- os: macos-15
|
||||||
name: MacOS 15
|
name: MacOS 15
|
||||||
|
- os: macos-26
|
||||||
|
name: MacOS 26
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -34,7 +34,7 @@ jobs:
|
||||||
- name: Set Library Path
|
- name: Set Library Path
|
||||||
run: echo "DYLD_LIBRARY_PATH=/opt/homebrew/lib:\$DYLD_LIBRARY_PATH" >> $GITHUB_ENV
|
run: echo "DYLD_LIBRARY_PATH=/opt/homebrew/lib:\$DYLD_LIBRARY_PATH" >> $GITHUB_ENV
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./publish.cmd Release
|
run: dotnet run --project Projects/BuildTool -- --config Release --skip-prereqs
|
||||||
- name: Migration Changes
|
- name: Migration Changes
|
||||||
run: git diff --exit-code ./**/Migrations/*.v*.json
|
run: git diff --exit-code ./**/Migrations/*.v*.json
|
||||||
- name: Test
|
- name: Test
|
||||||
|
|
@ -90,6 +90,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
global-json-file: global.json
|
global-json-file: global.json
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./publish.cmd Release
|
run: dotnet run --project Projects/BuildTool -- --config Release --skip-prereqs
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test --no-restore
|
run: dotnet test --no-restore
|
||||||
|
|
|
||||||
91
.github/workflows/build-tool-release.yml
vendored
Normal file
91
.github/workflows/build-tool-release.yml
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
name: Build Tool Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'Projects/BuildTool/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
name: Build (${{ matrix.rid }})
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
rid: win-x64
|
||||||
|
artifact: build-tool-win-x64.exe
|
||||||
|
- os: windows-latest
|
||||||
|
rid: win-arm64
|
||||||
|
artifact: build-tool-win-arm64.exe
|
||||||
|
- os: macos-15
|
||||||
|
rid: osx-arm64
|
||||||
|
artifact: build-tool-osx-arm64
|
||||||
|
- os: ubuntu-latest
|
||||||
|
rid: linux-x64
|
||||||
|
artifact: build-tool-linux-x64
|
||||||
|
- os: ubuntu-24.04-arm
|
||||||
|
rid: linux-arm64
|
||||||
|
artifact: build-tool-linux-arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
global-json-file: global.json
|
||||||
|
|
||||||
|
- name: Publish NativeAOT
|
||||||
|
run: dotnet publish Projects/BuildTool/BuildTool.csproj -c Release -r ${{ matrix.rid }} -o publish/
|
||||||
|
|
||||||
|
- name: Rename artifact (Unix)
|
||||||
|
if: runner.os != 'Windows'
|
||||||
|
run: mv publish/build-tool publish/${{ matrix.artifact }}
|
||||||
|
|
||||||
|
- name: Rename artifact (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: mv publish/build-tool.exe publish/${{ matrix.artifact }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}
|
||||||
|
path: publish/${{ matrix.artifact }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts/
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Generate checksums
|
||||||
|
run: |
|
||||||
|
cd artifacts
|
||||||
|
sha256sum build-tool-* > checksums-sha256.txt
|
||||||
|
|
||||||
|
- name: Create or update release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: build-tool-latest
|
||||||
|
name: Build Tool (Latest)
|
||||||
|
body: |
|
||||||
|
Latest NativeAOT-compiled build tool binaries.
|
||||||
|
These are automatically downloaded by `publish.cmd` / `publish.sh`.
|
||||||
|
prerelease: true
|
||||||
|
files: |
|
||||||
|
artifacts/build-tool-*
|
||||||
|
artifacts/checksums-sha256.txt
|
||||||
|
make_latest: false
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -45,3 +45,6 @@
|
||||||
|
|
||||||
/packages/*
|
/packages/*
|
||||||
/Distribution/Configuration/server-access.json
|
/Distribution/Configuration/server-access.json
|
||||||
|
|
||||||
|
# BuildTool native binaries (downloaded from GitHub Releases)
|
||||||
|
/tools/
|
||||||
|
|
|
||||||
67
ModernUO.sln
67
ModernUO.sln
|
|
@ -1,67 +0,0 @@
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 18
|
|
||||||
VisualStudioVersion = 18.0.11222.15 d18.0
|
|
||||||
MinimumVisualStudioVersion = 18.0.11222.15
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Projects\Server\Server.csproj", "{5E93BB35-3661-4822-9A8A-859726BAD87F}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UOContent", "Projects\UOContent\UOContent.csproj", "{83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server.Tests", "Projects\Server.Tests\Server.Tests.csproj", "{D7A5D3AF-D607-46EF-BAAD-0D424190311F}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UOContent.Tests", "Projects\UOContent.Tests\UOContent.Tests.csproj", "{3C4797F9-603E-44EF-8E8C-9275CC9EA74B}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Projects\Application\Application.csproj", "{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logger", "Projects\Logger\Logger.csproj", "{ECAD3793-A7C5-4546-AA88-77DD24574410}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Analyze|Any CPU = Analyze|Any CPU
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{5E93BB35-3661-4822-9A8A-859726BAD87F}.Analyze|Any CPU.ActiveCfg = Analyze|Any CPU
|
|
||||||
{5E93BB35-3661-4822-9A8A-859726BAD87F}.Analyze|Any CPU.Build.0 = Analyze|Any CPU
|
|
||||||
{5E93BB35-3661-4822-9A8A-859726BAD87F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{5E93BB35-3661-4822-9A8A-859726BAD87F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{5E93BB35-3661-4822-9A8A-859726BAD87F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{5E93BB35-3661-4822-9A8A-859726BAD87F}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Analyze|Any CPU.ActiveCfg = Analyze|Any CPU
|
|
||||||
{83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Analyze|Any CPU.Build.0 = Analyze|Any CPU
|
|
||||||
{83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Analyze|Any CPU.ActiveCfg = Analyze|Any CPU
|
|
||||||
{D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Analyze|Any CPU.Build.0 = Analyze|Any CPU
|
|
||||||
{D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Analyze|Any CPU.ActiveCfg = Analyze|Any CPU
|
|
||||||
{3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Analyze|Any CPU.Build.0 = Analyze|Any CPU
|
|
||||||
{3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}.Analyze|Any CPU.ActiveCfg = Analyze|Any CPU
|
|
||||||
{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}.Analyze|Any CPU.Build.0 = Analyze|Any CPU
|
|
||||||
{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{ECAD3793-A7C5-4546-AA88-77DD24574410}.Analyze|Any CPU.ActiveCfg = Analyze|Any CPU
|
|
||||||
{ECAD3793-A7C5-4546-AA88-77DD24574410}.Analyze|Any CPU.Build.0 = Analyze|Any CPU
|
|
||||||
{ECAD3793-A7C5-4546-AA88-77DD24574410}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{ECAD3793-A7C5-4546-AA88-77DD24574410}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{ECAD3793-A7C5-4546-AA88-77DD24574410}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{ECAD3793-A7C5-4546-AA88-77DD24574410}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {8F69EF13-E193-40B9-BA9F-4862A9F0C038}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
21
ModernUO.slnx
Normal file
21
ModernUO.slnx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Solution>
|
||||||
|
<Configurations>
|
||||||
|
<BuildType Name="Analyze" />
|
||||||
|
<BuildType Name="Debug" />
|
||||||
|
<BuildType Name="Release" />
|
||||||
|
</Configurations>
|
||||||
|
<Project Path="Projects/Application/Application.csproj" />
|
||||||
|
<Project Path="Projects/BuildTool/BuildTool.csproj">
|
||||||
|
<BuildType Solution="Analyze|*" Project="Release" />
|
||||||
|
<Build Solution="Analyze|*" Project="false" />
|
||||||
|
<Build Solution="Debug|*" Project="false" />
|
||||||
|
</Project>
|
||||||
|
<Project Path="Projects/Logger/Logger.csproj" />
|
||||||
|
<Project Path="Projects/Server.Tests/Server.Tests.csproj" />
|
||||||
|
<Project Path="Projects/Server/Server.csproj" />
|
||||||
|
<Project Path="Projects/UOContent.Tests/UOContent.Tests.csproj" />
|
||||||
|
<Project Path="Projects/UOContent/UOContent.csproj" />
|
||||||
|
<Properties Name="Visual Studio">
|
||||||
|
<Property Name="MinimumVersion" Value="18.0.11222.15" />
|
||||||
|
</Properties>
|
||||||
|
</Solution>
|
||||||
55
Projects/BuildTool/Branding.cs
Normal file
55
Projects/BuildTool/Branding.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace BuildTool;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static branding assets for the BuildTool CLI.
|
||||||
|
/// Colors sourced from ModernUO brand palette (docs/packets/template.html, docs/branding/logo.svg).
|
||||||
|
/// </summary>
|
||||||
|
public static class Branding
|
||||||
|
{
|
||||||
|
// ModernUO brand gold palette (from the "U" in the logo)
|
||||||
|
public static readonly Color Gold = new(213, 191, 116); // #d5bf74 - primary brand gold
|
||||||
|
public static readonly Color GoldLight = new(223, 198, 136); // #dfc688 - interactive elements
|
||||||
|
public static readonly Color GoldMuted = new(232, 206, 161); // #e8cea1 - body text
|
||||||
|
public static readonly Color GoldDim = new(162, 145, 71); // #a29147 - dimmed gold
|
||||||
|
|
||||||
|
// Silver palette (from the "O" in the logo)
|
||||||
|
public static readonly Color Silver = new(223, 223, 221); // #dfdfdd - primary silver
|
||||||
|
public static readonly Color SilverDim = new(150, 150, 148); // #969694 - dimmed silver
|
||||||
|
|
||||||
|
// Functional colors
|
||||||
|
public static readonly Color Success = new(34, 197, 94); // #22c55e - green
|
||||||
|
public static readonly Color Info = new(59, 130, 246); // #3b82f6 - blue
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
public static readonly Style GoldStyle = new(Gold);
|
||||||
|
public static readonly Style SilverStyle = new(Silver);
|
||||||
|
public static readonly Style HighlightStyle = new(GoldLight);
|
||||||
|
public static readonly Style DimGoldStyle = new(GoldDim);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The iconic ModernUO circular UO monogram with true-color ANSI escape codes.
|
||||||
|
/// Converted from the brand PNG (docs/branding/android-chrome-512x512.png).
|
||||||
|
/// Gold U on the left, silver O on the right, rendered with per-character RGB colors.
|
||||||
|
/// Output with Console.Write() to bypass Spectre markup parsing.
|
||||||
|
/// </summary>
|
||||||
|
public const string UOLogoAnsi =
|
||||||
|
" \e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;2;2;1m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;19;17;10m \e[0m\e[38;2;56;49;31m.\e[0m\e[38;2;46;41;24m.\e[0m\e[38;2;86;86;87m-\e[0m\e[38;2;134;133;131m+\e[0m\e[38;2;136;135;134m+\e[0m\e[38;2;133;132;131m+\e[0m\e[38;2;119;118;117m=\e[0m\e[38;2;87;86;85m-\e[0m\e[38;2;52;52;51m:\e[0m\e[38;2;20;20;20m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;2;2;2m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\n" +
|
||||||
|
" \e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;19;17;11m \e[0m\e[38;2;97;87;52m-\e[0m\e[38;2;163;145;87m+\e[0m\e[38;2;184;164;101m*\e[0m\e[38;2;202;179;111m*\e[0m\e[38;2;135;121;74m=\e[0m\e[38;2;96;97;101m-\e[0m\e[38;2;134;134;131m+\e[0m\e[38;2;124;123;123m=\e[0m\e[38;2;131;131;130m+\e[0m\e[38;2;144;143;143m+\e[0m\e[38;2;197;196;195m#\e[0m\e[38;2;211;210;209m%\e[0m\e[38;2;195;194;193m#\e[0m\e[38;2;169;168;167m*\e[0m\e[38;2;99;99;98m-\e[0m\e[38;2;19;18;18m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\n" +
|
||||||
|
" \e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;6;6;4m \e[0m\e[38;2;120;109;64m=\e[0m\e[38;2;228;204;120m#\e[0m\e[38;2;218;196;118m#\e[0m\e[38;2;180;163;101m*\e[0m\e[38;2;229;206;122m%\e[0m\e[38;2;231;207;122m%\e[0m\e[38;2;145;130;80m+\e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;139;140;139m+\e[0m\e[38;2;236;237;236m@\e[0m\e[38;2;234;234;234m@\e[0m\e[38;2;183;183;182m#\e[0m\e[38;2;222;222;222m%\e[0m\e[38;2;231;231;231m@\e[0m\e[38;2;120;120;119m=\e[0m\e[38;2;6;6;6m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\n" +
|
||||||
|
" \e[38;2;0;0;0m \e[0m\e[38;2;18;17;11m \e[0m\e[38;2;180;162;93m*\e[0m\e[38;2;241;216;123m%\e[0m\e[38;2;172;156;94m*\e[0m\e[38;2;22;21;15m \e[0m\e[38;2;68;63;41m:\e[0m\e[38;2;240;216;123m%\e[0m\e[38;2;244;219;125m%\e[0m\e[38;2;150;135;80m+\e[0m\e[38;2;2;2;1m \e[0m\e[38;2;3;3;3m \e[0m\e[38;2;3;3;3m \e[0m\e[38;2;3;3;3m \e[0m\e[38;2;2;2;2m \e[0m\e[38;2;148;148;148m+\e[0m\e[38;2;244;245;245m@\e[0m\e[38;2;240;241;241m@\e[0m\e[38;2;66;66;66m:\e[0m\e[38;2;21;21;21m \e[0m\e[38;2;171;172;171m*\e[0m\e[38;2;241;242;242m@\e[0m\e[38;2;179;179;179m#\e[0m\e[38;2;17;17;17m \e[0m\e[38;2;0;0;0m \e[0m\n" +
|
||||||
|
" \e[38;2;1;1;2m \e[0m\e[38;2;136;120;70m=\e[0m\e[38;2;207;180;98m#\e[0m\e[38;2;146;130;76m+\e[0m\e[38;2;0;1;4m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;76;68;42m:\e[0m\e[38;2;204;178;98m*\e[0m\e[38;2;204;177;97m*\e[0m\e[38;2;135;119;70m=\e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;134;135;134m+\e[0m\e[38;2;208;209;209m%\e[0m\e[38;2;207;208;207m%\e[0m\e[38;2;75;75;75m:\e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;146;147;146m+\e[0m\e[38;2;211;212;212m%\e[0m\e[38;2;135;135;135m+\e[0m\e[38;2;0;0;0m \e[0m\n" +
|
||||||
|
" \e[38;2;33;30;21m.\e[0m\e[38;2;161;134;70m+\e[0m\e[38;2;161;133;69m+\e[0m\e[38;2;51;46;30m.\e[0m\e[38;2;0;0;0m \e[0m\e[38;2;1;1;0m \e[0m\e[38;2;67;59;37m:\e[0m\e[38;2;162;134;69m+\e[0m\e[38;2;158;129;65m+\e[0m\e[38;2;118;102;58m=\e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;118;118;118m=\e[0m\e[38;2;165;166;166m*\e[0m\e[38;2;167;168;168m*\e[0m\e[38;2;66;66;66m:\e[0m\e[38;2;1;1;1m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;50;50;50m.\e[0m\e[38;2;166;167;167m*\e[0m\e[38;2;165;166;165m*\e[0m\e[38;2;32;32;31m.\e[0m\n" +
|
||||||
|
" \e[38;2;35;32;23m.\e[0m\e[38;2;154;128;62m+\e[0m\e[38;2;154;127;61m+\e[0m\e[38;2;49;44;29m.\e[0m\e[38;2;0;0;0m \e[0m\e[38;2;1;1;0m \e[0m\e[38;2;66;59;36m:\e[0m\e[38;2;154;126;61m+\e[0m\e[38;2;150;121;56m=\e[0m\e[38;2;112;96;54m-\e[0m\e[38;2;0;0;0m \e[0m\e[38;2;3;2;0m \e[0m\e[38;2;3;3;1m \e[0m\e[38;2;2;2;1m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;118;119;118m=\e[0m\e[38;2;165;165;165m*\e[0m\e[38;2;167;168;168m*\e[0m\e[38;2;67;67;66m:\e[0m\e[38;2;1;1;1m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;48;48;47m.\e[0m\e[38;2;167;168;168m*\e[0m\e[38;2;166;167;166m*\e[0m\e[38;2;33;33;33m.\e[0m\n" +
|
||||||
|
" \e[38;2;3;3;5m \e[0m\e[38;2;130;114;62m=\e[0m\e[38;2;181;154;68m+\e[0m\e[38;2;132;115;59m=\e[0m\e[38;2;0;0;2m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;46;42;28m.\e[0m\e[38;2;184;159;76m*\e[0m\e[38;2;178;152;66m+\e[0m\e[38;2;151;132;66m+\e[0m\e[38;2;13;12;9m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;132;132;132m+\e[0m\e[38;2;212;213;213m%\e[0m\e[38;2;208;208;208m%\e[0m\e[38;2;50;50;49m.\e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;146;146;146m+\e[0m\e[38;2;213;213;213m%\e[0m\e[38;2;139;139;139m+\e[0m\e[38;2;1;1;1m \e[0m\n" +
|
||||||
|
" \e[38;2;0;0;0m \e[0m\e[38;2;22;21;16m \e[0m\e[38;2;161;142;72m+\e[0m\e[38;2;197;172;77m*\e[0m\e[38;2;138;122;60m=\e[0m\e[38;2;24;22;13m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;89;80;46m-\e[0m\e[38;2;185;162;76m*\e[0m\e[38;2;195;171;77m*\e[0m\e[38;2;192;168;78m*\e[0m\e[38;2;151;132;62m+\e[0m\e[38;2;136;119;56m=\e[0m\e[38;2;160;140;67m+\e[0m\e[38;2;115;100;48m-\e[0m\e[38;2;143;145;150m+\e[0m\e[38;2;220;219;218m%\e[0m\e[38;2;92;92;92m-\e[0m\e[38;2;0;0;0m \e[0m\e[38;2;24;24;24m \e[0m\e[38;2;155;155;155m*\e[0m\e[38;2;232;232;232m@\e[0m\e[38;2;180;180;180m#\e[0m\e[38;2;21;21;21m \e[0m\e[38;2;0;0;0m \e[0m\n" +
|
||||||
|
" \e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;11;11;11m \e[0m\e[38;2;109;97;54m-\e[0m\e[38;2;183;159;79m*\e[0m\e[38;2;181;157;78m*\e[0m\e[38;2;109;94;48m-\e[0m\e[38;2;36;32;19m.\e[0m\e[38;2;33;31;21m.\e[0m\e[38;2;68;62;36m:\e[0m\e[38;2;102;91;48m-\e[0m\e[38;2;126;111;57m=\e[0m\e[38;2;131;116;59m=\e[0m\e[38;2;127;113;58m=\e[0m\e[38;2;71;64;36m:\e[0m\e[38;2;37;37;40m.\e[0m\e[38;2;39;39;38m.\e[0m\e[38;2;38;38;38m.\e[0m\e[38;2;122;122;121m=\e[0m\e[38;2;204;204;204m%\e[0m\e[38;2;208;208;208m%\e[0m\e[38;2;116;116;115m=\e[0m\e[38;2;9;9;9m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\n" +
|
||||||
|
" \e[38;2;0;0;0m \e[0m\e[38;2;1;1;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;24;23;17m \e[0m\e[38;2;88;77;46m-\e[0m\e[38;2;142;123;67m=\e[0m\e[38;2;161;138;75m+\e[0m\e[38;2;143;121;65m=\e[0m\e[38;2;115;97;54m-\e[0m\e[38;2;95;80;46m-\e[0m\e[38;2;83;71;42m:\e[0m\e[38;2;79;67;40m:\e[0m\e[38;2;86;74;44m:\e[0m\e[38;2;62;54;30m:\e[0m\e[38;2;87;88;89m-\e[0m\e[38;2;160;160;159m*\e[0m\e[38;2;178;178;178m#\e[0m\e[38;2;156;156;156m*\e[0m\e[38;2;94;94;94m-\e[0m\e[38;2;22;22;22m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;1;1;1m \e[0m\e[38;2;0;0;0m \e[0m\n" +
|
||||||
|
" \e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;2;1;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;1m \e[0m\e[38;2;25;22;17m \e[0m\e[38;2;58;51;34m:\e[0m\e[38;2;87;75;47m:\e[0m\e[38;2;105;90;56m-\e[0m\e[38;2;115;98;60m-\e[0m\e[38;2;117;99;60m-\e[0m\e[38;2;115;99;60m-\e[0m\e[38;2;76;67;41m:\e[0m\e[38;2;57;58;60m:\e[0m\e[38;2;66;65;65m:\e[0m\e[38;2;23;23;23m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;2;2;2m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\e[38;2;0;0;0m \e[0m\n";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subtitle shown below the banner.
|
||||||
|
/// </summary>
|
||||||
|
public const string Subtitle = "Ultima Online Server Emulator";
|
||||||
|
}
|
||||||
17
Projects/BuildTool/BuildOptions.cs
Normal file
17
Projects/BuildTool/BuildOptions.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
namespace BuildTool;
|
||||||
|
|
||||||
|
public enum BuildAction
|
||||||
|
{
|
||||||
|
Publish,
|
||||||
|
Migrate
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class BuildOptions
|
||||||
|
{
|
||||||
|
public BuildAction Action { get; set; } = BuildAction.Publish;
|
||||||
|
public string Config { get; set; } = "Release";
|
||||||
|
public string? Os { get; set; }
|
||||||
|
public string? Arch { get; set; }
|
||||||
|
public bool SkipPrereqs { get; set; }
|
||||||
|
public bool Interactive { get; set; }
|
||||||
|
}
|
||||||
20
Projects/BuildTool/BuildTool.csproj
Normal file
20
Projects/BuildTool/BuildTool.csproj
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>Preview</LangVersion>
|
||||||
|
<ImportDirectoryBuildProps>false</ImportDirectoryBuildProps>
|
||||||
|
<RootNamespace>BuildTool</RootNamespace>
|
||||||
|
<AssemblyName>build-tool</AssemblyName>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<TrimMode>link</TrimMode>
|
||||||
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
|
<RuntimeIdentifiers>win-x64;win-arm64;osx-x64;osx-arm64;linux-x64;linux-arm64</RuntimeIdentifiers>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Spectre.Console" Version="0.54.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
25
Projects/BuildTool/Interactive/CancellationTracker.cs
Normal file
25
Projects/BuildTool/Interactive/CancellationTracker.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
namespace BuildTool.Interactive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks Ctrl+C press timing to detect double-press for exit.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CancellationTracker
|
||||||
|
{
|
||||||
|
private DateTime _lastCancelTime = DateTime.MinValue;
|
||||||
|
private const int DoublePressMs = 500;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if this is a double-press (within 500ms of last press).
|
||||||
|
/// Also updates the last press time.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDoublePress()
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var isDouble = (now - _lastCancelTime).TotalMilliseconds < DoublePressMs;
|
||||||
|
_lastCancelTime = now;
|
||||||
|
return isDouble;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Global instance for app-wide tracking.</summary>
|
||||||
|
public static CancellationTracker Instance { get; } = new();
|
||||||
|
}
|
||||||
365
Projects/BuildTool/Interactive/GuidedMode.cs
Normal file
365
Projects/BuildTool/Interactive/GuidedMode.cs
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
using BuildTool.Platform;
|
||||||
|
using BuildTool.Prerequisites;
|
||||||
|
using BuildTool.Publishing;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace BuildTool.Interactive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles interactive mode when the BuildTool is run without arguments.
|
||||||
|
/// Provides a polished menu-driven experience with proper Ctrl+C navigation.
|
||||||
|
/// Uses ModernUO brand gold color palette.
|
||||||
|
/// </summary>
|
||||||
|
public static class GuidedMode
|
||||||
|
{
|
||||||
|
// Brand gold as markup string for inline use
|
||||||
|
private const string GoldMarkup = "rgb(213,191,116)";
|
||||||
|
private const string GoldLightMarkup = "rgb(223,198,136)";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks menu depth. 0 = root menu, >0 = submenu.
|
||||||
|
/// Used by ShowPrompt to automatically add Back option in submenus.
|
||||||
|
/// </summary>
|
||||||
|
private static int _menuDepth;
|
||||||
|
|
||||||
|
public static int Run(PlatformInfo platform, string repoRoot)
|
||||||
|
{
|
||||||
|
return ShowMainMenu(platform, repoRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows a selection prompt with automatic Back support.
|
||||||
|
/// When _menuDepth > 0, appends a ":left_arrow: Back" choice.
|
||||||
|
/// Returns null if the user chose Back or pressed Ctrl+C in a submenu.
|
||||||
|
/// At the root menu, Ctrl+C requires double-press to exit (returns null on double-press).
|
||||||
|
/// </summary>
|
||||||
|
private static string? ShowPrompt(string title, params string[] choices)
|
||||||
|
{
|
||||||
|
var allChoices = choices.Where(c => !string.IsNullOrEmpty(c)).ToList();
|
||||||
|
|
||||||
|
if (_menuDepth > 0)
|
||||||
|
{
|
||||||
|
allChoices.Add(":left_arrow: Back");
|
||||||
|
}
|
||||||
|
|
||||||
|
var prompt = new SelectionPrompt<string>()
|
||||||
|
.Title($"[{GoldMarkup}]{title}[/]")
|
||||||
|
.PageSize(10)
|
||||||
|
.HighlightStyle(Branding.HighlightStyle)
|
||||||
|
.AddChoices(allChoices);
|
||||||
|
|
||||||
|
string selection;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
selection = prompt.Show(AnsiConsole.Console);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
InteractiveCancellation.Instance.Reset();
|
||||||
|
|
||||||
|
if (_menuDepth > 0)
|
||||||
|
{
|
||||||
|
return null; // Back
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root menu: require double-press to exit
|
||||||
|
if (CancellationTracker.Instance.IsDoublePress())
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("\n[grey]Exiting...[/]");
|
||||||
|
return ":cross_mark: Exit";
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.Write(new Text("\nPress Ctrl+C again to exit\n", Branding.DimGoldStyle));
|
||||||
|
Thread.Sleep(100);
|
||||||
|
return ""; // Signal to redraw menu
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.Contains("Back"))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ShowMainMenu(PlatformInfo platform, string repoRoot)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
AnsiConsole.Clear();
|
||||||
|
|
||||||
|
// Write the true-color ANSI logo directly (bypasses Spectre markup parsing)
|
||||||
|
Console.Write(Branding.UOLogoAnsi);
|
||||||
|
AnsiConsole.Write(new Text($" {Branding.Subtitle}\n", Branding.DimGoldStyle));
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
|
// Show environment info
|
||||||
|
var sdkVersion = GetDotNetSdkVersion();
|
||||||
|
var table = new Table()
|
||||||
|
.Border(TableBorder.Rounded)
|
||||||
|
.BorderColor(Branding.GoldDim)
|
||||||
|
.HideHeaders()
|
||||||
|
.AddColumn("Key")
|
||||||
|
.AddColumn("Value");
|
||||||
|
|
||||||
|
table.AddRow(
|
||||||
|
new Text("Platform", Branding.DimGoldStyle),
|
||||||
|
new Markup($"[white]{Markup.Escape(platform.OsName)} {platform.ArchRid}[/]")
|
||||||
|
);
|
||||||
|
table.AddRow(
|
||||||
|
new Text(".NET SDK", Branding.DimGoldStyle),
|
||||||
|
sdkVersion is not null
|
||||||
|
? new Markup($"[white]{Markup.Escape(sdkVersion)}[/]")
|
||||||
|
: new Markup("[red]Not found[/]")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (platform.DistroName is not null)
|
||||||
|
{
|
||||||
|
table.AddRow(
|
||||||
|
new Text("Distribution", Branding.DimGoldStyle),
|
||||||
|
new Markup($"[white]{Markup.Escape(platform.DistroName)}[/]")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform.KernelVersion is not null)
|
||||||
|
{
|
||||||
|
table.AddRow(
|
||||||
|
new Text("Kernel", Branding.DimGoldStyle),
|
||||||
|
new Markup($"[white]{Markup.Escape(platform.KernelVersion)}[/]")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.Write(table);
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
|
var selection = ShowPrompt(
|
||||||
|
"What would you like to do?",
|
||||||
|
":hammer: Publish Server",
|
||||||
|
":stethoscope: Check Prerequisites",
|
||||||
|
"",
|
||||||
|
":cross_mark: Exit"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(selection))
|
||||||
|
{
|
||||||
|
continue; // Redraw (single Ctrl+C)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.Contains("Publish"))
|
||||||
|
{
|
||||||
|
_menuDepth++;
|
||||||
|
AnsiConsole.Clear();
|
||||||
|
AnsiConsole.MarkupLine($"[bold {GoldMarkup}]:hammer: PUBLISH SERVER[/]\n");
|
||||||
|
var result = RunPublish(platform, repoRoot);
|
||||||
|
_menuDepth--;
|
||||||
|
|
||||||
|
if (result is not null) // null = Back, skip WaitForKey
|
||||||
|
{
|
||||||
|
WaitForKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (selection.Contains("Prerequisites"))
|
||||||
|
{
|
||||||
|
_menuDepth++;
|
||||||
|
AnsiConsole.Clear();
|
||||||
|
AnsiConsole.MarkupLine($"[bold {GoldMarkup}]:stethoscope: PREREQUISITE CHECK[/]\n");
|
||||||
|
PrerequisiteChecker.CheckAll(platform, repoRoot, interactive: true);
|
||||||
|
_menuDepth--;
|
||||||
|
WaitForKey();
|
||||||
|
}
|
||||||
|
else if (selection.Contains("Exit"))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns null if user navigated Back to main menu, 0 on success, non-zero on error.
|
||||||
|
/// Uses a step loop so Back goes to the previous step, not the main menu.
|
||||||
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// Returns null if user navigated Back to main menu, 0 on success, non-zero on error.
|
||||||
|
/// Uses a step loop so Back goes to the previous step, not the main menu.
|
||||||
|
/// </summary>
|
||||||
|
private static int? RunPublish(PlatformInfo platform, string repoRoot)
|
||||||
|
{
|
||||||
|
// Step 0: SDK check (always needed for building)
|
||||||
|
if (!PrerequisiteChecker.CheckSdk(platform, repoRoot, interactive: true))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = "Release";
|
||||||
|
var rid = platform.Rid;
|
||||||
|
var os = platform.OsRid;
|
||||||
|
var arch = platform.ArchRid;
|
||||||
|
|
||||||
|
// Steps 1+: interactive prompts with back navigation
|
||||||
|
var step = 1;
|
||||||
|
while (step <= 4)
|
||||||
|
{
|
||||||
|
switch (step)
|
||||||
|
{
|
||||||
|
case 1: // Configuration
|
||||||
|
{
|
||||||
|
var choice = ShowPrompt("Configuration:", "Release (Recommended)", "Debug");
|
||||||
|
if (choice is null)
|
||||||
|
{
|
||||||
|
return null; // Back from first prompt = back to main menu
|
||||||
|
}
|
||||||
|
|
||||||
|
config = choice.Contains("Release") ? "Release" : "Debug";
|
||||||
|
step++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: // Platform confirmation
|
||||||
|
{
|
||||||
|
var choice = ShowPrompt(
|
||||||
|
$"Target platform: [{GoldLightMarkup}]{platform.Rid}[/] [grey](auto-detected)[/]",
|
||||||
|
$":check_mark: Use {platform.Rid}",
|
||||||
|
":wrench: Choose different platform"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (choice is null)
|
||||||
|
{
|
||||||
|
step--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (choice.Contains("Choose"))
|
||||||
|
{
|
||||||
|
step++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rid = platform.Rid;
|
||||||
|
os = platform.OsRid;
|
||||||
|
step = 5; // Skip OS/arch selection, go to publish
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: // OS selection (only if custom platform)
|
||||||
|
{
|
||||||
|
var choice = ShowPrompt(
|
||||||
|
"Operating System:",
|
||||||
|
"win (Windows)",
|
||||||
|
"osx (macOS)",
|
||||||
|
"linux (Linux)"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (choice is null)
|
||||||
|
{
|
||||||
|
step--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
os = choice.Split(' ')[0].Trim();
|
||||||
|
step++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 4: // Architecture selection (only if custom platform)
|
||||||
|
{
|
||||||
|
var choice = ShowPrompt(
|
||||||
|
"Architecture:",
|
||||||
|
"x64 (Intel/AMD 64-bit)",
|
||||||
|
"arm64 (ARM 64-bit)"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (choice is null)
|
||||||
|
{
|
||||||
|
step--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
arch = choice.Split(' ')[0].Trim();
|
||||||
|
rid = $"{os}-{arch}";
|
||||||
|
step++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isCrossCompile = os != platform.OsRid;
|
||||||
|
|
||||||
|
// Native library checks — only relevant when targeting the current OS
|
||||||
|
if (!isCrossCompile)
|
||||||
|
{
|
||||||
|
if (!PrerequisiteChecker.CheckNativeLibraries(platform, interactive: true))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run publish
|
||||||
|
var exitCode = PublishOrchestrator.Run(config, rid, interactive: true, isCrossCompile);
|
||||||
|
|
||||||
|
if (exitCode == 0)
|
||||||
|
{
|
||||||
|
CheckFirstTimeSetup(repoRoot);
|
||||||
|
|
||||||
|
if (isCrossCompile)
|
||||||
|
{
|
||||||
|
DisplayTargetRequirements(os, rid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DisplayTargetRequirements(string targetOs, string rid)
|
||||||
|
{
|
||||||
|
var (osName, commands) = NativeLibraryChecker.GetRequirementsForTarget(targetOs);
|
||||||
|
|
||||||
|
var rows = new List<Spectre.Console.Rendering.IRenderable>
|
||||||
|
{
|
||||||
|
new Markup($"[bold {GoldMarkup}]:clipboard: Prerequisites for {Markup.Escape(osName)} target ({rid})[/]"),
|
||||||
|
new Markup("")
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 0; i < commands.Length; i++)
|
||||||
|
{
|
||||||
|
var cmd = commands[i];
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
rows.Add(new Markup(""));
|
||||||
|
}
|
||||||
|
rows.Add(new Markup($" [grey]\u2022[/] {Markup.Escape(cmd)}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.Add(new Markup(""));
|
||||||
|
rows.Add(new Markup(""));
|
||||||
|
rows.Add(new Markup("[grey]Install these on the target machine, then copy the [/][rgb(213,191,116)]Distribution[/] [grey]folder to the server and run it.[/]"));
|
||||||
|
|
||||||
|
AnsiConsole.Write(new Panel(new Rows(rows))
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Branding.GoldDim)
|
||||||
|
.Padding(1, 0));
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckFirstTimeSetup(string repoRoot)
|
||||||
|
{
|
||||||
|
var configPath = Path.Combine(repoRoot, "Distribution", "Configuration", "modernuo.json");
|
||||||
|
if (!File.Exists(configPath))
|
||||||
|
{
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
AnsiConsole.Write(new Text(
|
||||||
|
":light_bulb: Tip: The server will prompt you for game data file locations on first run.\n",
|
||||||
|
Branding.DimGoldStyle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetDotNetSdkVersion()
|
||||||
|
{
|
||||||
|
var result = ProcessRunner.RunCaptured("dotnet", "--version");
|
||||||
|
return result.Success ? result.StandardOutput.Trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WaitForKey()
|
||||||
|
{
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
AnsiConsole.MarkupLine("[grey]Press any key to continue...[/]");
|
||||||
|
Console.ReadKey(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
Projects/BuildTool/Interactive/InteractiveCancellation.cs
Normal file
58
Projects/BuildTool/Interactive/InteractiveCancellation.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
namespace BuildTool.Interactive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages cancellation for interactive mode.
|
||||||
|
/// When Ctrl+C is pressed, the current token is cancelled, causing prompts to throw OperationCanceledException.
|
||||||
|
/// The token source is then reset so the next prompt can work.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InteractiveCancellation : IDisposable
|
||||||
|
{
|
||||||
|
private CancellationTokenSource _cts = new();
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current cancellation token to pass to prompts.
|
||||||
|
/// </summary>
|
||||||
|
public CancellationToken Token
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _cts.Token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signals cancellation (called from Ctrl+C handler).
|
||||||
|
/// </summary>
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_cts.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the cancellation token source so new prompts can work.
|
||||||
|
/// Call this after catching OperationCanceledException.
|
||||||
|
/// </summary>
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_cts.Dispose();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Global instance for app-wide interactive cancellation.</summary>
|
||||||
|
public static InteractiveCancellation Instance { get; } = new();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cts.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Projects/BuildTool/Json/GlobalJsonContext.cs
Normal file
25
Projects/BuildTool/Json/GlobalJsonContext.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace BuildTool.Json;
|
||||||
|
|
||||||
|
public sealed class GlobalJson
|
||||||
|
{
|
||||||
|
[JsonPropertyName("sdk")]
|
||||||
|
public SdkConfig? Sdk { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SdkConfig
|
||||||
|
{
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string? Version { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("rollForward")]
|
||||||
|
public string? RollForward { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("allowPrerelease")]
|
||||||
|
public bool AllowPrerelease { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(GlobalJson))]
|
||||||
|
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||||
|
internal partial class GlobalJsonContext : JsonSerializerContext;
|
||||||
212
Projects/BuildTool/Platform/PlatformDetector.cs
Normal file
212
Projects/BuildTool/Platform/PlatformDetector.cs
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using BuildTool.Publishing;
|
||||||
|
|
||||||
|
namespace BuildTool.Platform;
|
||||||
|
|
||||||
|
public static class PlatformDetector
|
||||||
|
{
|
||||||
|
// Windows 11 starts at build 22000
|
||||||
|
private const int Windows11MinBuild = 22000;
|
||||||
|
|
||||||
|
public static PlatformInfo Detect()
|
||||||
|
{
|
||||||
|
var osRid = GetOsRid();
|
||||||
|
var archRid = GetArchRid();
|
||||||
|
var (distroId, distroName) = GetLinuxDistroInfo();
|
||||||
|
var kernelVersion = GetKernelVersion(osRid);
|
||||||
|
var osName = GetOsDisplayName(osRid, distroName, kernelVersion);
|
||||||
|
var packageManager = DetectPackageManager(osRid, distroId);
|
||||||
|
|
||||||
|
return new PlatformInfo
|
||||||
|
{
|
||||||
|
OsName = osName,
|
||||||
|
OsRid = osRid,
|
||||||
|
ArchRid = archRid,
|
||||||
|
DistroId = distroId,
|
||||||
|
DistroName = distroName,
|
||||||
|
KernelVersion = kernelVersion,
|
||||||
|
PackageManager = packageManager
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetOsDisplayName(string osRid, string? distroName, string? kernelVersion)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
var version = Environment.OSVersion.Version;
|
||||||
|
var windowsVersion = version.Build >= Windows11MinBuild ? "11" : "10";
|
||||||
|
return $"Windows {windowsVersion} (Build {version.Build})";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
var version = Environment.OSVersion.Version;
|
||||||
|
var macosName = GetMacOSCodename(version.Major);
|
||||||
|
return macosName is not null
|
||||||
|
? $"macOS {version.Major}.{version.Minor} {macosName}"
|
||||||
|
: $"macOS {version.Major}.{version.Minor}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux: show distro name with kernel version
|
||||||
|
if (distroName is not null && kernelVersion is not null)
|
||||||
|
{
|
||||||
|
return $"{distroName} (kernel {kernelVersion})";
|
||||||
|
}
|
||||||
|
|
||||||
|
return distroName ?? (kernelVersion is not null ? $"Linux (kernel {kernelVersion})" : "Linux");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetMacOSCodename(int majorVersion) =>
|
||||||
|
majorVersion switch
|
||||||
|
{
|
||||||
|
15 => "Sequoia",
|
||||||
|
14 => "Sonoma",
|
||||||
|
13 => "Ventura",
|
||||||
|
12 => "Monterey",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string GetOsRid()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return "win";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
return "osx";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "linux";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetArchRid() =>
|
||||||
|
RuntimeInformation.ProcessArchitecture switch
|
||||||
|
{
|
||||||
|
Architecture.X64 => "x64",
|
||||||
|
Architecture.Arm64 => "arm64",
|
||||||
|
_ => "x64"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string? GetKernelVersion(string osRid)
|
||||||
|
{
|
||||||
|
if (osRid != "linux")
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try /proc/version first (most reliable)
|
||||||
|
if (File.Exists("/proc/version"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var procVersion = File.ReadAllText("/proc/version");
|
||||||
|
// Format: "Linux version 6.5.0-44-generic ..."
|
||||||
|
var parts = procVersion.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Length >= 3 && parts[0] == "Linux" && parts[1] == "version")
|
||||||
|
{
|
||||||
|
return parts[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fall through to uname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to uname -r
|
||||||
|
var result = ProcessRunner.RunCaptured("uname", "-r");
|
||||||
|
return result.Success ? result.StandardOutput.Trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string? Id, string? Name) GetLinuxDistroInfo()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const string osReleasePath = "/etc/os-release";
|
||||||
|
if (!File.Exists(osReleasePath))
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
string? id = null;
|
||||||
|
string? name = null;
|
||||||
|
|
||||||
|
foreach (var line in File.ReadLines(osReleasePath))
|
||||||
|
{
|
||||||
|
if (line.StartsWith("ID=", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
id = line[3..].Trim('"');
|
||||||
|
}
|
||||||
|
else if (line.StartsWith("PRETTY_NAME=", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
name = line[12..].Trim('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id is not null && name is not null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PackageManager DetectPackageManager(string osRid, string? distroId)
|
||||||
|
{
|
||||||
|
if (osRid == "osx")
|
||||||
|
{
|
||||||
|
return PackageManager.Brew;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (osRid != "linux")
|
||||||
|
{
|
||||||
|
return PackageManager.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check distro ID first for accuracy
|
||||||
|
return distroId?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"ubuntu" or "debian" or "linuxmint" or "pop" or "elementary" or "zorin" => PackageManager.Apt,
|
||||||
|
"fedora" or "centos" or "rhel" or "rocky" or "alma" or "ol" => PackageManager.Dnf,
|
||||||
|
"opensuse" or "opensuse-leap" or "opensuse-tumbleweed" or "sles" => PackageManager.Zypper,
|
||||||
|
"arch" or "manjaro" or "endeavouros" => PackageManager.Pacman,
|
||||||
|
"alpine" => PackageManager.Apk,
|
||||||
|
_ => DetectPackageManagerFromBinaries()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PackageManager DetectPackageManagerFromBinaries()
|
||||||
|
{
|
||||||
|
if (File.Exists("/usr/bin/apt-get") || File.Exists("/usr/bin/apt"))
|
||||||
|
{
|
||||||
|
return PackageManager.Apt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists("/usr/bin/dnf"))
|
||||||
|
{
|
||||||
|
return PackageManager.Dnf;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists("/usr/bin/zypper"))
|
||||||
|
{
|
||||||
|
return PackageManager.Zypper;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists("/usr/bin/pacman"))
|
||||||
|
{
|
||||||
|
return PackageManager.Pacman;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists("/sbin/apk"))
|
||||||
|
{
|
||||||
|
return PackageManager.Apk;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PackageManager.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Projects/BuildTool/Platform/PlatformInfo.cs
Normal file
28
Projects/BuildTool/Platform/PlatformInfo.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace BuildTool.Platform;
|
||||||
|
|
||||||
|
public enum PackageManager
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Apt,
|
||||||
|
Dnf,
|
||||||
|
Zypper,
|
||||||
|
Pacman,
|
||||||
|
Apk,
|
||||||
|
Brew
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlatformInfo
|
||||||
|
{
|
||||||
|
public required string OsName { get; init; }
|
||||||
|
public required string OsRid { get; init; }
|
||||||
|
public required string ArchRid { get; init; }
|
||||||
|
public string Rid => $"{OsRid}-{ArchRid}";
|
||||||
|
public string? DistroId { get; init; }
|
||||||
|
public string? DistroName { get; init; }
|
||||||
|
public string? KernelVersion { get; init; }
|
||||||
|
public PackageManager PackageManager { get; init; } = PackageManager.Unknown;
|
||||||
|
|
||||||
|
public bool IsWindows => OsRid == "win";
|
||||||
|
public bool IsMacOS => OsRid == "osx";
|
||||||
|
public bool IsLinux => OsRid == "linux";
|
||||||
|
}
|
||||||
190
Projects/BuildTool/Prerequisites/DotNetSdkManager.cs
Normal file
190
Projects/BuildTool/Prerequisites/DotNetSdkManager.cs
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using BuildTool.Json;
|
||||||
|
using BuildTool.Platform;
|
||||||
|
using BuildTool.Publishing;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace BuildTool.Prerequisites;
|
||||||
|
|
||||||
|
public static partial class DotNetSdkManager
|
||||||
|
{
|
||||||
|
private const string DotNetDownloadUrl = "https://dotnet.microsoft.com/download";
|
||||||
|
private const string InstallScriptUrlWindows = "https://dot.net/v1/dotnet-install.ps1";
|
||||||
|
private const string InstallScriptUrlUnix = "https://dot.net/v1/dotnet-install.sh";
|
||||||
|
|
||||||
|
public static PrerequisiteResult CheckSdk(string repoRoot)
|
||||||
|
{
|
||||||
|
var requiredVersionStr = ReadRequiredVersion(repoRoot) ?? "10.0.201";
|
||||||
|
|
||||||
|
if (!Version.TryParse(requiredVersionStr, out var requiredVersion))
|
||||||
|
{
|
||||||
|
requiredVersion = new Version(10, 0, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if dotnet is on PATH
|
||||||
|
var result = ProcessRunner.RunCaptured("dotnet", "--list-sdks");
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = ".NET SDK",
|
||||||
|
Passed = false,
|
||||||
|
Details = $".NET SDK {requiredVersionStr}+ is not installed",
|
||||||
|
DownloadUrl = DotNetDownloadUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse installed SDK versions — find the best match
|
||||||
|
Version? bestVersion = null;
|
||||||
|
string? bestVersionStr = null;
|
||||||
|
|
||||||
|
foreach (var line in result.StandardOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
var match = SdkVersionRegex().Match(line);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionStr = match.Groups[1].Value;
|
||||||
|
if (!Version.TryParse(versionStr, out var version))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestVersion is null || version > bestVersion)
|
||||||
|
{
|
||||||
|
bestVersion = version;
|
||||||
|
bestVersionStr = versionStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestVersion is null || bestVersion < requiredVersion)
|
||||||
|
{
|
||||||
|
return new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = ".NET SDK",
|
||||||
|
Passed = false,
|
||||||
|
Details = bestVersionStr is not null
|
||||||
|
? $".NET SDK {bestVersionStr} found, but {requiredVersionStr}+ is required"
|
||||||
|
: $".NET SDK {requiredVersionStr}+ is required but no SDK was found",
|
||||||
|
DownloadUrl = DotNetDownloadUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = ".NET SDK",
|
||||||
|
Passed = true,
|
||||||
|
Details = $".NET SDK {bestVersionStr}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool OfferInstall(PlatformInfo platform, bool interactive)
|
||||||
|
{
|
||||||
|
if (!interactive)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("[red]Error:[/] .NET SDK is not installed. Download from:");
|
||||||
|
AnsiConsole.MarkupLine($"[link]{DotNetDownloadUrl}[/]");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var install = AnsiConsole.Confirm(
|
||||||
|
"[yellow].NET SDK is required but not found.[/] Would you like to install it now?",
|
||||||
|
defaultValue: true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!install)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($"\nDownload the .NET SDK from: [link]{DotNetDownloadUrl}[/]");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RunInstallScript(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RunInstallScript(PlatformInfo platform)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("\n[blue]Installing .NET SDK...[/]");
|
||||||
|
|
||||||
|
if (platform.IsWindows)
|
||||||
|
{
|
||||||
|
return RunWindowsInstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
return RunUnixInstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RunWindowsInstall()
|
||||||
|
{
|
||||||
|
// Download and run the official Microsoft install script
|
||||||
|
var result = ProcessRunner.RunPassthrough(
|
||||||
|
"powershell",
|
||||||
|
$"-NoProfile -ExecutionPolicy Bypass -Command \"& {{ " +
|
||||||
|
$"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; " +
|
||||||
|
$"$script = Invoke-WebRequest -Uri '{InstallScriptUrlWindows}' -UseBasicParsing; " +
|
||||||
|
$"$scriptBlock = [scriptblock]::Create($script.Content); " +
|
||||||
|
$"& $scriptBlock -Channel 10.0 }}\""
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("[red]Failed to install .NET SDK.[/]");
|
||||||
|
AnsiConsole.MarkupLine($"Please install manually from: [link]{DotNetDownloadUrl}[/]");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine("[green]Successfully installed .NET SDK.[/]");
|
||||||
|
AnsiConsole.MarkupLine("[yellow]Note:[/] You may need to restart your terminal for the PATH to update.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RunUnixInstall()
|
||||||
|
{
|
||||||
|
// Download and run the official Microsoft install script
|
||||||
|
var result = ProcessRunner.RunPassthrough(
|
||||||
|
"bash",
|
||||||
|
$"-c \"curl -fsSL {InstallScriptUrlUnix} | bash -s -- --channel 10.0\""
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("[red]Failed to install .NET SDK.[/]");
|
||||||
|
AnsiConsole.MarkupLine($"Please install manually from: [link]{DotNetDownloadUrl}[/]");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine("[green]Successfully installed .NET SDK.[/]");
|
||||||
|
AnsiConsole.MarkupLine("[yellow]Note:[/] You may need to add ~/.dotnet to your PATH or restart your terminal.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ReadRequiredVersion(string repoRoot)
|
||||||
|
{
|
||||||
|
var globalJsonPath = Path.Combine(repoRoot, "global.json");
|
||||||
|
if (!File.Exists(globalJsonPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = File.ReadAllText(globalJsonPath);
|
||||||
|
var globalJson = JsonSerializer.Deserialize(json, GlobalJsonContext.Default.GlobalJson);
|
||||||
|
return globalJson?.Sdk?.Version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kept for reference but no longer used — Version.TryParse handles comparison now
|
||||||
|
private static int ParseMajorVersion(string version)
|
||||||
|
{
|
||||||
|
var dotIndex = version.IndexOf('.');
|
||||||
|
if (dotIndex <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return int.TryParse(version.AsSpan(0, dotIndex), out var major) ? major : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^(\d+\.\d+\.\d+\S*)")]
|
||||||
|
private static partial Regex SdkVersionRegex();
|
||||||
|
}
|
||||||
309
Projects/BuildTool/Prerequisites/NativeLibraryChecker.cs
Normal file
309
Projects/BuildTool/Prerequisites/NativeLibraryChecker.cs
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
using BuildTool.Platform;
|
||||||
|
using BuildTool.Publishing;
|
||||||
|
|
||||||
|
namespace BuildTool.Prerequisites;
|
||||||
|
|
||||||
|
public static class NativeLibraryChecker
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the required prerequisites for a target OS (without checking the local machine).
|
||||||
|
/// Used to inform users what they need to install on the deployment target after cross-compiling.
|
||||||
|
/// </summary>
|
||||||
|
public static (string Description, string[] InstallCommands) GetRequirementsForTarget(string targetOs)
|
||||||
|
{
|
||||||
|
return targetOs switch
|
||||||
|
{
|
||||||
|
"win" => (
|
||||||
|
"Windows",
|
||||||
|
[
|
||||||
|
".NET 10 Runtime — https://dotnet.microsoft.com/download/dotnet/10.0",
|
||||||
|
"VC++ Redistributable v14 — https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"osx" => (
|
||||||
|
"macOS",
|
||||||
|
[
|
||||||
|
".NET 10 Runtime — https://dotnet.microsoft.com/download/dotnet/10.0",
|
||||||
|
"brew install icu4c libdeflate zstd argon2"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"linux" => (
|
||||||
|
"Linux",
|
||||||
|
[
|
||||||
|
".NET 10 Runtime — https://dotnet.microsoft.com/download/dotnet/10.0",
|
||||||
|
"Debian/Ubuntu: sudo apt-get install -y libicu-dev libdeflate-dev zstd libargon2-dev liburing-dev",
|
||||||
|
"Fedora/RHEL: sudo dnf install -y libicu libdeflate-devel zstd libargon2-devel liburing-devel",
|
||||||
|
"CentOS: Also requires epel-release and CRB enabled"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
_ => ("Unknown", [".NET 10 Runtime — https://dotnet.microsoft.com/download/dotnet/10.0"])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PrerequisiteResult> Check(PlatformInfo platform)
|
||||||
|
{
|
||||||
|
if (platform.IsWindows)
|
||||||
|
{
|
||||||
|
return CheckWindows(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform.IsMacOS)
|
||||||
|
{
|
||||||
|
return CheckMacOS();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform.IsLinux)
|
||||||
|
{
|
||||||
|
return CheckLinux(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PrerequisiteResult> CheckWindows(PlatformInfo platform)
|
||||||
|
{
|
||||||
|
var results = new List<PrerequisiteResult>();
|
||||||
|
|
||||||
|
// Check VC++ Redistributable via registry
|
||||||
|
var vcRedistInstalled = CheckVcRedist(platform.ArchRid);
|
||||||
|
var downloadUrl = platform.ArchRid == "arm64"
|
||||||
|
? "https://aka.ms/vs/17/release/vc_redist.arm64.exe"
|
||||||
|
: "https://aka.ms/vs/17/release/vc_redist.x64.exe";
|
||||||
|
|
||||||
|
results.Add(new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = "VC++ Redistributable v14",
|
||||||
|
Passed = vcRedistInstalled,
|
||||||
|
Details = vcRedistInstalled ? "Installed" : "Not found",
|
||||||
|
DownloadUrl = vcRedistInstalled ? null : downloadUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckVcRedist(string arch)
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check multiple known registry paths for VC++ 14.x Redistributable
|
||||||
|
string[] registryPaths =
|
||||||
|
[
|
||||||
|
$@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\{arch}",
|
||||||
|
$@"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\{arch}"
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (var path in registryPaths)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(path);
|
||||||
|
if (key?.GetValue("Installed") is int installed && installed == 1)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Registry access may fail, continue checking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PrerequisiteResult> CheckMacOS()
|
||||||
|
{
|
||||||
|
var results = new List<PrerequisiteResult>();
|
||||||
|
|
||||||
|
// Check if Homebrew is installed
|
||||||
|
var brewResult = ProcessRunner.RunCaptured("which", "brew");
|
||||||
|
if (!brewResult.Success)
|
||||||
|
{
|
||||||
|
results.Add(new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = "Homebrew",
|
||||||
|
Passed = false,
|
||||||
|
Details = "Homebrew is required to install native dependencies",
|
||||||
|
DownloadUrl = "https://brew.sh"
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required Homebrew formulae
|
||||||
|
var formulae = new[] { "icu4c", "libdeflate", "zstd", "argon2" };
|
||||||
|
var listResult = ProcessRunner.RunCaptured("brew", "list --formula");
|
||||||
|
var installedFormulae = listResult.Success
|
||||||
|
? new HashSet<string>(
|
||||||
|
listResult.StandardOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries),
|
||||||
|
StringComparer.OrdinalIgnoreCase)
|
||||||
|
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var missing = new List<string>();
|
||||||
|
foreach (var formula in formulae)
|
||||||
|
{
|
||||||
|
var installed = installedFormulae.Contains(formula);
|
||||||
|
if (!installed)
|
||||||
|
{
|
||||||
|
missing.Add(formula);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add(new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = formula,
|
||||||
|
Passed = installed,
|
||||||
|
Details = installed ? "Installed" : "Not installed",
|
||||||
|
InstallCommand = installed ? null : $"brew install {formula}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.Count > 0)
|
||||||
|
{
|
||||||
|
results.Add(new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = "Install all missing",
|
||||||
|
Passed = false,
|
||||||
|
IsWarning = true,
|
||||||
|
Details = "Run the following command to install all missing dependencies:",
|
||||||
|
InstallCommand = $"brew install {string.Join(' ', missing)}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PrerequisiteResult> CheckLinux(PlatformInfo platform)
|
||||||
|
{
|
||||||
|
return platform.PackageManager switch
|
||||||
|
{
|
||||||
|
PackageManager.Apt => CheckLinuxApt(),
|
||||||
|
PackageManager.Dnf => CheckLinuxDnf(platform),
|
||||||
|
_ => CheckLinuxGeneric(platform)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PrerequisiteResult> CheckLinuxApt()
|
||||||
|
{
|
||||||
|
var results = new List<PrerequisiteResult>();
|
||||||
|
var packages = new[] { "libicu-dev", "libdeflate-dev", "zstd", "libargon2-dev", "liburing-dev" };
|
||||||
|
var missing = new List<string>();
|
||||||
|
|
||||||
|
foreach (var package in packages)
|
||||||
|
{
|
||||||
|
var result = ProcessRunner.RunCaptured("dpkg", $"-l {package}");
|
||||||
|
var installed = result.Success && result.StandardOutput.Contains("ii");
|
||||||
|
|
||||||
|
if (!installed)
|
||||||
|
{
|
||||||
|
missing.Add(package);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add(new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = package,
|
||||||
|
Passed = installed,
|
||||||
|
Details = installed ? "Installed" : "Not installed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.Count > 0)
|
||||||
|
{
|
||||||
|
results.Add(new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = "Install all missing",
|
||||||
|
Passed = false,
|
||||||
|
IsWarning = true,
|
||||||
|
Details = "Run the following command to install all missing dependencies:",
|
||||||
|
InstallCommand = $"sudo apt-get install -y {string.Join(' ', missing)}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PrerequisiteResult> CheckLinuxDnf(PlatformInfo platform)
|
||||||
|
{
|
||||||
|
var results = new List<PrerequisiteResult>();
|
||||||
|
var packages = new[] { "libicu", "libdeflate-devel", "zstd", "libargon2-devel", "liburing-devel" };
|
||||||
|
var missing = new List<string>();
|
||||||
|
|
||||||
|
foreach (var package in packages)
|
||||||
|
{
|
||||||
|
var result = ProcessRunner.RunCaptured("rpm", $"-q {package}");
|
||||||
|
var installed = result.Success;
|
||||||
|
|
||||||
|
if (!installed)
|
||||||
|
{
|
||||||
|
missing.Add(package);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add(new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = package,
|
||||||
|
Passed = installed,
|
||||||
|
Details = installed ? "Installed" : "Not installed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is CentOS (needs EPEL)
|
||||||
|
var isCentOs = platform.DistroId?.Equals("centos", StringComparison.OrdinalIgnoreCase) == true;
|
||||||
|
if (isCentOs && missing.Count > 0)
|
||||||
|
{
|
||||||
|
results.Add(new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = "EPEL Repository",
|
||||||
|
Passed = false,
|
||||||
|
IsWarning = true,
|
||||||
|
Details = "CentOS requires EPEL for some packages. Enable it first:",
|
||||||
|
InstallCommand = "sudo dnf install -y epel-release epel-next-release && sudo dnf config-manager --set-enabled crb"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.Count > 0)
|
||||||
|
{
|
||||||
|
results.Add(new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = "Install all missing",
|
||||||
|
Passed = false,
|
||||||
|
IsWarning = true,
|
||||||
|
Details = "Run the following command to install all missing dependencies:",
|
||||||
|
InstallCommand = $"sudo dnf install -y {string.Join(' ', missing)}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PrerequisiteResult> CheckLinuxGeneric(PlatformInfo platform)
|
||||||
|
{
|
||||||
|
var results = new List<PrerequisiteResult>();
|
||||||
|
|
||||||
|
// Use ldconfig to check for shared libraries
|
||||||
|
var ldResult = ProcessRunner.RunCaptured("ldconfig", "-p");
|
||||||
|
var ldOutput = ldResult.Success ? ldResult.StandardOutput : "";
|
||||||
|
|
||||||
|
var libraries = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["libicu"] = "libicuuc",
|
||||||
|
["libdeflate"] = "libdeflate",
|
||||||
|
["zstd"] = "libzstd",
|
||||||
|
["libargon2"] = "libargon2",
|
||||||
|
["liburing"] = "liburing"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (name, soName) in libraries)
|
||||||
|
{
|
||||||
|
var found = ldOutput.Contains(soName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
results.Add(new PrerequisiteResult
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Passed = found,
|
||||||
|
Details = found ? "Found" : "Not found — install using your package manager"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
165
Projects/BuildTool/Prerequisites/PrerequisiteChecker.cs
Normal file
165
Projects/BuildTool/Prerequisites/PrerequisiteChecker.cs
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
using BuildTool.Platform;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace BuildTool.Prerequisites;
|
||||||
|
|
||||||
|
public static class PrerequisiteChecker
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Runs all prerequisite checks (SDK + native libraries) and displays results.
|
||||||
|
/// Returns true if all critical prerequisites pass.
|
||||||
|
/// </summary>
|
||||||
|
public static bool CheckAll(PlatformInfo platform, string repoRoot, bool interactive)
|
||||||
|
{
|
||||||
|
var panel = new Panel("[bold]Checking prerequisites[/]")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Branding.Gold)
|
||||||
|
.Padding(1, 0);
|
||||||
|
AnsiConsole.Write(panel);
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
|
if (!CheckSdkInternal(platform, repoRoot, interactive))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckNativeLibrariesInternal(platform, interactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks only the .NET SDK (sufficient for cross-compilation).
|
||||||
|
/// Returns true if the SDK is available.
|
||||||
|
/// </summary>
|
||||||
|
public static bool CheckSdk(PlatformInfo platform, string repoRoot, bool interactive)
|
||||||
|
{
|
||||||
|
var panel = new Panel("[bold]Checking .NET SDK[/]")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Branding.Gold)
|
||||||
|
.Padding(1, 0);
|
||||||
|
AnsiConsole.Write(panel);
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
|
var passed = CheckSdkInternal(platform, repoRoot, interactive);
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
return passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks native libraries for the current platform.
|
||||||
|
/// Call this only when the publish target matches the current OS.
|
||||||
|
/// Returns true if all pass or the user chose to continue.
|
||||||
|
/// </summary>
|
||||||
|
public static bool CheckNativeLibraries(PlatformInfo platform, bool interactive)
|
||||||
|
{
|
||||||
|
AnsiConsole.Write(new Panel("[bold]Checking native libraries[/]")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Branding.Gold)
|
||||||
|
.Padding(1, 0));
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
|
var passed = CheckNativeLibrariesInternal(platform, interactive);
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
return passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckSdkInternal(PlatformInfo platform, string repoRoot, bool interactive)
|
||||||
|
{
|
||||||
|
var sdkResult = DotNetSdkManager.CheckSdk(repoRoot);
|
||||||
|
DisplayResult(sdkResult);
|
||||||
|
|
||||||
|
if (!sdkResult.Passed)
|
||||||
|
{
|
||||||
|
var installed = DotNetSdkManager.OfferInstall(platform, interactive);
|
||||||
|
if (!installed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sdkResult = DotNetSdkManager.CheckSdk(repoRoot);
|
||||||
|
if (!sdkResult.Passed)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("[red]SDK installation may require a terminal restart.[/]");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayResult(sdkResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckNativeLibrariesInternal(PlatformInfo platform, bool interactive)
|
||||||
|
{
|
||||||
|
var nativeResults = NativeLibraryChecker.Check(platform);
|
||||||
|
var hasMissing = false;
|
||||||
|
|
||||||
|
foreach (var result in nativeResults)
|
||||||
|
{
|
||||||
|
if (result.IsWarning)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayResult(result);
|
||||||
|
if (!result.Passed)
|
||||||
|
{
|
||||||
|
hasMissing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMissing)
|
||||||
|
{
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
|
foreach (var result in nativeResults)
|
||||||
|
{
|
||||||
|
if (!result.IsWarning)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.InstallCommand is not null)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($" [yellow]:warning: {result.Details}[/]");
|
||||||
|
AnsiConsole.MarkupLine($" [white on grey23] {result.InstallCommand} [/]");
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interactive)
|
||||||
|
{
|
||||||
|
var continueAnyway = AnsiConsole.Confirm(
|
||||||
|
"[yellow]Some prerequisites are missing. Continue anyway?[/]",
|
||||||
|
defaultValue: false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!continueAnyway)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return interactive; // In interactive mode, user chose to continue
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine("[green]:check_mark_button: All prerequisites satisfied.[/]");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DisplayResult(PrerequisiteResult result)
|
||||||
|
{
|
||||||
|
if (result.IsWarning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var icon = result.Passed ? "[green]:check_mark:[/]" : "[red]:cross_mark:[/]";
|
||||||
|
var details = result.Details is not null ? $" [grey]({Markup.Escape(result.Details)})[/]" : "";
|
||||||
|
AnsiConsole.MarkupLine($" {icon} {Markup.Escape(result.Name)}{details}");
|
||||||
|
|
||||||
|
if (!result.Passed && result.DownloadUrl is not null)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($" [grey]Download: {result.DownloadUrl}[/]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Projects/BuildTool/Prerequisites/PrerequisiteResult.cs
Normal file
11
Projects/BuildTool/Prerequisites/PrerequisiteResult.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace BuildTool.Prerequisites;
|
||||||
|
|
||||||
|
public sealed class PrerequisiteResult
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public required bool Passed { get; init; }
|
||||||
|
public string? Details { get; init; }
|
||||||
|
public string? InstallCommand { get; init; }
|
||||||
|
public string? DownloadUrl { get; init; }
|
||||||
|
public bool IsWarning { get; init; }
|
||||||
|
}
|
||||||
173
Projects/BuildTool/Program.cs
Normal file
173
Projects/BuildTool/Program.cs
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
using System.Text;
|
||||||
|
using BuildTool;
|
||||||
|
using BuildTool.Interactive;
|
||||||
|
using BuildTool.Platform;
|
||||||
|
using BuildTool.Prerequisites;
|
||||||
|
using BuildTool.Publishing;
|
||||||
|
|
||||||
|
Console.OutputEncoding = Encoding.UTF8;
|
||||||
|
|
||||||
|
var repoRoot = FindRepoRoot();
|
||||||
|
if (repoRoot is null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Error: Could not find ModernUO.slnx. Run this tool from the repository root.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = ParseArguments(args);
|
||||||
|
|
||||||
|
// Interactive mode: no args provided
|
||||||
|
if (options.Interactive)
|
||||||
|
{
|
||||||
|
// Handle Ctrl+C by signaling our cancellation token (not killing the process)
|
||||||
|
Console.CancelKeyPress += (_, e) =>
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
InteractiveCancellation.Instance.Cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
var platform = PlatformDetector.Detect();
|
||||||
|
return GuidedMode.Run(platform, repoRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-interactive mode
|
||||||
|
var detectedPlatform = PlatformDetector.Detect();
|
||||||
|
options.Os ??= detectedPlatform.OsRid;
|
||||||
|
options.Arch ??= detectedPlatform.ArchRid;
|
||||||
|
var rid = $"{options.Os}-{options.Arch}";
|
||||||
|
|
||||||
|
// Run prerequisite checks unless skipped
|
||||||
|
if (!options.SkipPrereqs)
|
||||||
|
{
|
||||||
|
var sdkResult = DotNetSdkManager.CheckSdk(repoRoot);
|
||||||
|
if (!sdkResult.Passed)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: {sdkResult.Details}");
|
||||||
|
if (sdkResult.DownloadUrl is not null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Download: {sdkResult.DownloadUrl}");
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.Action switch
|
||||||
|
{
|
||||||
|
BuildAction.Publish => PublishOrchestrator.Run(options.Config, rid, interactive: false),
|
||||||
|
BuildAction.Migrate => SchemaMigrator.Run(interactive: false),
|
||||||
|
_ => 1
|
||||||
|
};
|
||||||
|
|
||||||
|
static BuildOptions ParseArguments(string[] args)
|
||||||
|
{
|
||||||
|
var options = new BuildOptions();
|
||||||
|
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
options.Interactive = true;
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for named arguments first
|
||||||
|
var hasNamedArgs = false;
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
switch (args[i])
|
||||||
|
{
|
||||||
|
case "--config" when i + 1 < args.Length:
|
||||||
|
{
|
||||||
|
options.Config = NormalizeConfig(args[++i]);
|
||||||
|
hasNamedArgs = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "--os" when i + 1 < args.Length:
|
||||||
|
{
|
||||||
|
options.Os = args[++i].ToLowerInvariant();
|
||||||
|
hasNamedArgs = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "--arch" when i + 1 < args.Length:
|
||||||
|
{
|
||||||
|
options.Arch = args[++i].ToLowerInvariant();
|
||||||
|
hasNamedArgs = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "--action" when i + 1 < args.Length:
|
||||||
|
{
|
||||||
|
options.Action = args[++i].ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"migrate" => BuildAction.Migrate,
|
||||||
|
_ => BuildAction.Publish
|
||||||
|
};
|
||||||
|
hasNamedArgs = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "--skip-prereqs":
|
||||||
|
{
|
||||||
|
options.SkipPrereqs = true;
|
||||||
|
hasNamedArgs = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "--interactive":
|
||||||
|
{
|
||||||
|
options.Interactive = true;
|
||||||
|
hasNamedArgs = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNamedArgs)
|
||||||
|
{
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positional argument parsing (backward compat: [config] [os] [arch])
|
||||||
|
if (args.Length >= 1)
|
||||||
|
{
|
||||||
|
options.Config = NormalizeConfig(args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Length >= 2)
|
||||||
|
{
|
||||||
|
options.Os = args[1].ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Length >= 3)
|
||||||
|
{
|
||||||
|
options.Arch = args[2].ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string NormalizeConfig(string config) =>
|
||||||
|
config.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"release" => "Release",
|
||||||
|
"debug" => "Debug",
|
||||||
|
_ => char.ToUpperInvariant(config[0]) + config[1..].ToLowerInvariant()
|
||||||
|
};
|
||||||
|
|
||||||
|
static string? FindRepoRoot()
|
||||||
|
{
|
||||||
|
// Check current directory first
|
||||||
|
var current = Directory.GetCurrentDirectory();
|
||||||
|
if (File.Exists(Path.Combine(current, "ModernUO.slnx")))
|
||||||
|
{
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk up to find the solution file
|
||||||
|
var dir = new DirectoryInfo(current);
|
||||||
|
while (dir?.Parent is not null)
|
||||||
|
{
|
||||||
|
dir = dir.Parent;
|
||||||
|
if (File.Exists(Path.Combine(dir.FullName, "ModernUO.slnx")))
|
||||||
|
{
|
||||||
|
return dir.FullName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
95
Projects/BuildTool/Publishing/ProcessRunner.cs
Normal file
95
Projects/BuildTool/Publishing/ProcessRunner.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace BuildTool.Publishing;
|
||||||
|
|
||||||
|
public sealed class ProcessResult
|
||||||
|
{
|
||||||
|
public required int ExitCode { get; init; }
|
||||||
|
public required string StandardOutput { get; init; }
|
||||||
|
public required string StandardError { get; init; }
|
||||||
|
public bool Success => ExitCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ProcessRunner
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Runs a command with output captured (for parsing results).
|
||||||
|
/// </summary>
|
||||||
|
public static ProcessResult RunCaptured(string fileName, string arguments, string? workingDirectory = null)
|
||||||
|
{
|
||||||
|
var stdout = new StringBuilder();
|
||||||
|
var stderr = new StringBuilder();
|
||||||
|
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Arguments = arguments,
|
||||||
|
WorkingDirectory = workingDirectory ?? Directory.GetCurrentDirectory(),
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using var process = Process.Start(psi);
|
||||||
|
if (process is null)
|
||||||
|
{
|
||||||
|
return new ProcessResult
|
||||||
|
{
|
||||||
|
ExitCode = -1,
|
||||||
|
StandardOutput = "",
|
||||||
|
StandardError = $"Failed to start process: {fileName}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
process.OutputDataReceived += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Data is not null)
|
||||||
|
{
|
||||||
|
stdout.AppendLine(e.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process.ErrorDataReceived += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Data is not null)
|
||||||
|
{
|
||||||
|
stderr.AppendLine(e.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
return new ProcessResult
|
||||||
|
{
|
||||||
|
ExitCode = process.ExitCode,
|
||||||
|
StandardOutput = stdout.ToString(),
|
||||||
|
StandardError = stderr.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs a command with output passed through to the console (for build commands in non-interactive mode).
|
||||||
|
/// </summary>
|
||||||
|
public static int RunPassthrough(string fileName, string arguments, string? workingDirectory = null)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Arguments = arguments,
|
||||||
|
WorkingDirectory = workingDirectory ?? Directory.GetCurrentDirectory(),
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
|
||||||
|
using var process = Process.Start(psi);
|
||||||
|
if (process is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.WaitForExit();
|
||||||
|
return process.ExitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
182
Projects/BuildTool/Publishing/PublishOrchestrator.cs
Normal file
182
Projects/BuildTool/Publishing/PublishOrchestrator.cs
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
using BuildTool.Platform;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace BuildTool.Publishing;
|
||||||
|
|
||||||
|
public static class PublishOrchestrator
|
||||||
|
{
|
||||||
|
// Target the Application project (not the solution) to avoid building BuildTool and test projects
|
||||||
|
private const string AppProject = "Projects/Application/Application.csproj";
|
||||||
|
|
||||||
|
private static readonly (string Description, string Command, string Arguments)[] BuildSteps =
|
||||||
|
[
|
||||||
|
("Restoring tools", "dotnet", "tool restore"),
|
||||||
|
("Cleaning project", "dotnet", $"clean {AppProject} --verbosity quiet"),
|
||||||
|
("Restoring packages", "dotnet", $"restore {AppProject} --force-evaluate --source https://api.nuget.org/v3/index.json"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public static int Run(string config, PlatformInfo platform, bool interactive)
|
||||||
|
{
|
||||||
|
return Run(config, platform.Rid, interactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int Run(string config, string rid, bool interactive, bool isCrossCompile = false)
|
||||||
|
{
|
||||||
|
if (interactive)
|
||||||
|
{
|
||||||
|
return RunInteractive(config, rid, isCrossCompile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RunNonInteractive(config, rid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RunInteractive(string config, string rid, bool isCrossCompile)
|
||||||
|
{
|
||||||
|
var panel = new Panel($"[bold]Publishing[/] [rgb(223,198,136)]{config}[/] for [rgb(223,198,136)]{rid}[/]")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Branding.Gold)
|
||||||
|
.Padding(1, 0);
|
||||||
|
AnsiConsole.Write(panel);
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
|
var allSteps = new List<(string Description, string Command, string Arguments)>(BuildSteps)
|
||||||
|
{
|
||||||
|
($"Publishing ({config}, {rid})", "dotnet", $"publish {AppProject} -c {config} -r {rid} --no-restore --self-contained=false"),
|
||||||
|
("Generating serialization schema", "dotnet", "tool run ModernUOSchemaGenerator -- ModernUO.slnx")
|
||||||
|
};
|
||||||
|
|
||||||
|
var completed = 0;
|
||||||
|
var total = allSteps.Count;
|
||||||
|
|
||||||
|
foreach (var (description, command, arguments) in allSteps)
|
||||||
|
{
|
||||||
|
completed++;
|
||||||
|
var exitCode = RunStepInteractive(description, command, arguments, completed, total);
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
AnsiConsole.Write(new Panel("[red bold]Build failed.[/] See error output above for details.")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Color.Red)
|
||||||
|
.Padding(1, 0));
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
DisplaySuccess(rid, isCrossCompile);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RunStepInteractive(string description, string command, string arguments, int step, int total)
|
||||||
|
{
|
||||||
|
var exitCode = 0;
|
||||||
|
string errorOutput = "";
|
||||||
|
|
||||||
|
AnsiConsole.Status()
|
||||||
|
.Spinner(Spinner.Known.Dots)
|
||||||
|
.SpinnerStyle(Branding.GoldStyle)
|
||||||
|
.Start($"[grey]({step}/{total})[/] {description}...", _ =>
|
||||||
|
{
|
||||||
|
var result = ProcessRunner.RunCaptured(command, arguments);
|
||||||
|
exitCode = result.ExitCode;
|
||||||
|
errorOutput = result.StandardError;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exitCode == 0)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($" [green]:check_mark:[/] [grey]({step}/{total})[/] {description}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($" [red]:cross_mark:[/] [grey]({step}/{total})[/] {description} [red](failed)[/]");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(errorOutput))
|
||||||
|
{
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
AnsiConsole.Write(new Panel(Markup.Escape(errorOutput.Trim()))
|
||||||
|
.Header("[red]Error Output[/]")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Color.Red)
|
||||||
|
.Expand());
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
AnsiConsole.MarkupLine($" [grey]Command: {command} {arguments}[/]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RunNonInteractive(string config, string rid)
|
||||||
|
{
|
||||||
|
// Run common build steps
|
||||||
|
foreach (var (description, command, arguments) in BuildSteps)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{command} {arguments}");
|
||||||
|
var exitCode = ProcessRunner.RunPassthrough(command, arguments);
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: '{description}' failed with exit code {exitCode}");
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish step
|
||||||
|
{
|
||||||
|
var publishArgs = $"publish {AppProject} -c {config} -r {rid} --no-restore --self-contained=false";
|
||||||
|
Console.WriteLine($"dotnet {publishArgs}");
|
||||||
|
var exitCode = ProcessRunner.RunPassthrough("dotnet", publishArgs);
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: 'publish' failed with exit code {exitCode}");
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema generation
|
||||||
|
{
|
||||||
|
Console.WriteLine("Generating serialization migration schema...");
|
||||||
|
const string schemaArgs = "tool run ModernUOSchemaGenerator -- ModernUO.slnx";
|
||||||
|
var exitCode = ProcessRunner.RunPassthrough("dotnet", schemaArgs);
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: schema generation failed with exit code {exitCode}");
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void DisplaySuccess(string rid, bool isCrossCompile = false)
|
||||||
|
{
|
||||||
|
var isWindows = rid.StartsWith("win", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var runCommand = isWindows ? "ModernUO.exe" : "dotnet ModernUO.dll";
|
||||||
|
|
||||||
|
var rows = new List<Spectre.Console.Rendering.IRenderable>
|
||||||
|
{
|
||||||
|
new Markup("[green bold]:check_mark_button: Build complete![/]"),
|
||||||
|
new Markup("")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isCrossCompile)
|
||||||
|
{
|
||||||
|
rows.Add(new Markup($"Copy the [rgb(213,191,116)]Distribution[/] folder to your [rgb(213,191,116)]{rid}[/] server, then run:"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rows.Add(new Markup("Run the server from the [rgb(213,191,116)]Distribution[/] directory:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.Add(new Markup(""));
|
||||||
|
rows.Add(new Markup(" [white on grey23] cd Distribution [/]"));
|
||||||
|
rows.Add(new Markup($" [white on grey23] {runCommand} [/]"));
|
||||||
|
|
||||||
|
AnsiConsole.Write(new Panel(new Rows(rows))
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Color.Green)
|
||||||
|
.Padding(1, 0));
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
126
Projects/BuildTool/Publishing/SchemaMigrator.cs
Normal file
126
Projects/BuildTool/Publishing/SchemaMigrator.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace BuildTool.Publishing;
|
||||||
|
|
||||||
|
public static class SchemaMigrator
|
||||||
|
{
|
||||||
|
public static int Run(bool interactive)
|
||||||
|
{
|
||||||
|
if (interactive)
|
||||||
|
{
|
||||||
|
return RunInteractive();
|
||||||
|
}
|
||||||
|
|
||||||
|
return RunNonInteractive();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RunInteractive()
|
||||||
|
{
|
||||||
|
var panel = new Panel("[bold]Running schema migration[/]")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Branding.Gold)
|
||||||
|
.Padding(1, 0);
|
||||||
|
AnsiConsole.Write(panel);
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
|
// Tool restore
|
||||||
|
{
|
||||||
|
var exitCode = 0;
|
||||||
|
string errorOutput = "";
|
||||||
|
|
||||||
|
AnsiConsole.Status()
|
||||||
|
.Spinner(Spinner.Known.Dots)
|
||||||
|
.SpinnerStyle(Branding.GoldStyle)
|
||||||
|
.Start("[grey](1/2)[/] Restoring tools...", _ =>
|
||||||
|
{
|
||||||
|
var result = ProcessRunner.RunCaptured("dotnet", "tool restore");
|
||||||
|
exitCode = result.ExitCode;
|
||||||
|
errorOutput = result.StandardError;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exitCode == 0)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine(" [green]:check_mark:[/] [grey](1/2)[/] Tools restored");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine(" [red]:cross_mark:[/] [grey](1/2)[/] Tool restore [red](failed)[/]");
|
||||||
|
if (!string.IsNullOrWhiteSpace(errorOutput))
|
||||||
|
{
|
||||||
|
AnsiConsole.Write(new Panel(Markup.Escape(errorOutput.Trim()))
|
||||||
|
.Header("[red]Error[/]")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Color.Red));
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema generation
|
||||||
|
{
|
||||||
|
var exitCode = 0;
|
||||||
|
string errorOutput = "";
|
||||||
|
|
||||||
|
AnsiConsole.Status()
|
||||||
|
.Spinner(Spinner.Known.Dots)
|
||||||
|
.SpinnerStyle(Branding.GoldStyle)
|
||||||
|
.Start("[grey](2/2)[/] Generating serialization schema...", _ =>
|
||||||
|
{
|
||||||
|
var result = ProcessRunner.RunCaptured(
|
||||||
|
"dotnet",
|
||||||
|
"tool run ModernUOSchemaGenerator -- ModernUO.slnx"
|
||||||
|
);
|
||||||
|
exitCode = result.ExitCode;
|
||||||
|
errorOutput = result.StandardError;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exitCode == 0)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine(" [green]:check_mark:[/] [grey](2/2)[/] Schema generated");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine(" [red]:cross_mark:[/] [grey](2/2)[/] Schema generation [red](failed)[/]");
|
||||||
|
if (!string.IsNullOrWhiteSpace(errorOutput))
|
||||||
|
{
|
||||||
|
AnsiConsole.Write(new Panel(Markup.Escape(errorOutput.Trim()))
|
||||||
|
.Header("[red]Error[/]")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Color.Red));
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
AnsiConsole.Write(new Panel("[green bold]:check_mark_button: Schema migration complete.[/]")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.BorderColor(Color.Green)
|
||||||
|
.Padding(1, 0));
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RunNonInteractive()
|
||||||
|
{
|
||||||
|
Console.WriteLine("dotnet tool restore");
|
||||||
|
var exitCode = ProcessRunner.RunPassthrough("dotnet", "tool restore");
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: tool restore failed with exit code {exitCode}");
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Generating serialization migration schema...");
|
||||||
|
exitCode = ProcessRunner.RunPassthrough("dotnet", "tool run ModernUOSchemaGenerator -- ModernUO.slnx");
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: schema generation failed with exit code {exitCode}");
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,12 +35,12 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Logger\Logger.csproj" />
|
<ProjectReference Include="..\Logger\Logger.csproj" />
|
||||||
<PackageReference Include="IORingGroup" Version="1.0.6" />
|
<PackageReference Include="IORingGroup" Version="1.0.6" />
|
||||||
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
|
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.2" />
|
||||||
<PackageReference Include="LibDeflate.Bindings" Version="1.0.3" />
|
<PackageReference Include="LibDeflate.Bindings" Version="1.0.3" />
|
||||||
<PackageReference Include="System.IO.Hashing" Version="10.0.3" />
|
<PackageReference Include="System.IO.Hashing" Version="10.0.5" />
|
||||||
|
|
||||||
<PackageReference Include="ModernUO.Serialization.Annotations" Version="2.14.2" />
|
<PackageReference Include="ModernUO.Serialization.Annotations" Version="2.14.2" />
|
||||||
<PackageReference Include="ModernUO.Serialization.Generator" Version="2.14.2" />
|
<PackageReference Include="ModernUO.Serialization.Generator" Version="2.14.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AdditionalFiles Include="Migrations/*.v*.json" />
|
<AdditionalFiles Include="Migrations/*.v*.json" />
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,15 @@
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
<PackageReference Include="LibDeflate.Bindings" Version="1.0.3" />
|
<PackageReference Include="LibDeflate.Bindings" Version="1.0.3" />
|
||||||
<PackageReference Include="MailKit" Version="4.15.1" />
|
<PackageReference Include="MailKit" Version="4.15.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.5" />
|
||||||
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
|
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.2" />
|
||||||
<PackageReference Include="Argon2.Bindings" Version="1.17.0" />
|
<PackageReference Include="Argon2.Bindings" Version="1.17.0" />
|
||||||
<PackageReference Include="ModernUO.CodeGeneratedEvents.Annotations" Version="1.0.0" />
|
<PackageReference Include="ModernUO.CodeGeneratedEvents.Annotations" Version="1.0.0" />
|
||||||
<PackageReference Include="ModernUO.CodeGeneratedEvents.Generator" Version="1.0.3.2" PrivateAssets="all" />
|
<PackageReference Include="ModernUO.CodeGeneratedEvents.Generator" Version="1.0.3.2" PrivateAssets="all" />
|
||||||
<PackageReference Include="ZstdNet" Version="1.5.7" />
|
<PackageReference Include="ZstdNet" Version="1.5.7" />
|
||||||
|
|
||||||
<PackageReference Include="ModernUO.Serialization.Annotations" Version="2.14.2" />
|
<PackageReference Include="ModernUO.Serialization.Annotations" Version="2.14.2" />
|
||||||
<PackageReference Include="ModernUO.Serialization.Generator" Version="2.14.2" />
|
<PackageReference Include="ModernUO.Serialization.Generator" Version="2.14.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AdditionalFiles Include="Migrations/*.v*.json" />
|
<AdditionalFiles Include="Migrations/*.v*.json" />
|
||||||
|
|
|
||||||
19
README.md
19
README.md
|
|
@ -56,14 +56,25 @@ ModernUO [] [os] [arch (default: x64)]`
|
|
||||||
|
#### Interactive Mode (Recommended for new users)
|
||||||
|
Run `./publish.cmd` (Windows) or `./publish.sh` (Linux/macOS) with no arguments to launch the guided build tool. It will:
|
||||||
|
- Check prerequisites (.NET SDK, native libraries)
|
||||||
|
- Walk you through configuration and platform selection
|
||||||
|
- Build and publish the server to the `Distribution` directory
|
||||||
|
- Show deployment instructions for cross-compiled builds
|
||||||
|
|
||||||
|
#### Command Line
|
||||||
|
```shell
|
||||||
|
./publish.cmd [release|debug] [os] [arch]
|
||||||
|
```
|
||||||
- `os` - [Supported operating systems](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md)
|
- `os` - [Supported operating systems](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md)
|
||||||
- `win` - [Windows](https://learn.microsoft.com/en-us/dotnet/core/install/windows)
|
- `win` - [Windows](https://learn.microsoft.com/en-us/dotnet/core/install/windows)
|
||||||
- `osx` - [MacOS](https://learn.microsoft.com/en-us/dotnet/core/install/macos)
|
- `osx` - [macOS](https://learn.microsoft.com/en-us/dotnet/core/install/macos)
|
||||||
- `linux` - [Linux](https://learn.microsoft.com/en-us/dotnet/core/install/linux)
|
- `linux` - [Linux](https://learn.microsoft.com/en-us/dotnet/core/install/linux)
|
||||||
- `arch`
|
- `arch`
|
||||||
- `x64` - Intel/AMD 64-bit
|
- `x64` - Intel/AMD 64-bit
|
||||||
- `arm64` - ARM 64-bit (Windows not supported)
|
- `arm64` - ARM 64-bit
|
||||||
|
|
||||||
## Linux Prerequisites
|
## Linux Prerequisites
|
||||||
### Fedora, CentOS, RHEL, etc
|
### Fedora, CentOS, RHEL, etc
|
||||||
|
|
@ -87,7 +98,9 @@ brew install icu4c libdeflate zstd argon2
|
||||||
|
|
||||||
## Running the Server
|
## Running the Server
|
||||||
- Follow the [publish](https://github.com/modernuo/ModernUO#buildingpublishing) instructions
|
- Follow the [publish](https://github.com/modernuo/ModernUO#buildingpublishing) instructions
|
||||||
|
- The `Distribution` directory is portable — copy it to your production server for deployment
|
||||||
- Run `ModernUO.exe` or `dotnet ModernUO.dll` from the `Distribution` directory
|
- Run `ModernUO.exe` or `dotnet ModernUO.dll` from the `Distribution` directory
|
||||||
|
- On first run, the server will prompt you to configure game data file locations
|
||||||
|
|
||||||
## Troubleshooting / FAQ
|
## Troubleshooting / FAQ
|
||||||
- See [FAQ](./FAQ.md)
|
- See [FAQ](./FAQ.md)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ jobs:
|
||||||
inputs:
|
inputs:
|
||||||
useGlobalJson: true
|
useGlobalJson: true
|
||||||
- task: NuGetAuthenticate@1
|
- task: NuGetAuthenticate@1
|
||||||
- script: ./publish.cmd Release
|
- script: dotnet run --project Projects\BuildTool -- --config Release --skip-prereqs
|
||||||
displayName: 'Build'
|
displayName: 'Build'
|
||||||
- powershell: ./.github/porcelain.ps1
|
- powershell: ./.github/porcelain.ps1
|
||||||
displayName: Migration Changes
|
displayName: Migration Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "10.0.100",
|
"version": "10.0.201",
|
||||||
"rollForward": "latestMajor",
|
"rollForward": "latestMajor",
|
||||||
"allowPrerelease": false
|
"allowPrerelease": false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
publish.cmd
45
publish.cmd
|
|
@ -1,46 +1,9 @@
|
||||||
:<<"::SHELLSCRIPT"
|
:<<"::SHELLSCRIPT"
|
||||||
@ECHO OFF
|
@ECHO OFF
|
||||||
GOTO :CMDSCRIPT
|
powershell -ExecutionPolicy Bypass -NoProfile -File "%~dp0publish.ps1" %*
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
|
|
||||||
::SHELLSCRIPT
|
::SHELLSCRIPT
|
||||||
|
# If run from bash (e.g., CI on Linux/macOS), dispatch to publish.sh
|
||||||
path=$(dirname "$0")
|
path=$(dirname "$0")
|
||||||
cd $path
|
exec "$path/publish.sh" "$@"
|
||||||
./publish.sh
|
|
||||||
exit $?
|
|
||||||
|
|
||||||
:CMDSCRIPT
|
|
||||||
IF "%~1" == "" (
|
|
||||||
SET config=-c Release
|
|
||||||
) ELSE (
|
|
||||||
IF "%~1" == "release" (
|
|
||||||
SET config=-c Release
|
|
||||||
) ELSE (
|
|
||||||
SET config=-c Debug
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
IF "%~2" == "" (
|
|
||||||
SET os=-r win
|
|
||||||
) ELSE (
|
|
||||||
SET os=-r %~2
|
|
||||||
)
|
|
||||||
|
|
||||||
IF "%~3" == "" (
|
|
||||||
SET arch=x64
|
|
||||||
) ELSE (
|
|
||||||
SET arch=%~3
|
|
||||||
)
|
|
||||||
|
|
||||||
echo dotnet tool restore
|
|
||||||
dotnet tool restore
|
|
||||||
|
|
||||||
echo dotnet clean --verbosity quiet
|
|
||||||
dotnet clean --verbosity quiet
|
|
||||||
echo dotnet restore --force-evaluate --source https://api.nuget.org/v3/index.json
|
|
||||||
dotnet restore --force-evaluate --source https://api.nuget.org/v3/index.json
|
|
||||||
|
|
||||||
echo dotnet publish %config% %os%-%arch% --no-restore --self-contained=false
|
|
||||||
dotnet publish %config% %os%-%arch% --no-restore --self-contained=false
|
|
||||||
|
|
||||||
echo Generating serialization migration schema...
|
|
||||||
dotnet tool run ModernUOSchemaGenerator -- ModernUO.sln
|
|
||||||
|
|
|
||||||
79
publish.ps1
Normal file
79
publish.ps1
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
$toolBinary = Join-Path (Join-Path $repoRoot 'tools') 'build-tool.exe'
|
||||||
|
$buildToolProject = Join-Path (Join-Path (Join-Path $repoRoot 'Projects') 'BuildTool') 'BuildTool.csproj'
|
||||||
|
|
||||||
|
function Get-BuildToolFromRelease {
|
||||||
|
try {
|
||||||
|
# Determine platform
|
||||||
|
$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' }
|
||||||
|
$assetName = "build-tool-win-$arch.exe"
|
||||||
|
|
||||||
|
Write-Host "Downloading build tool..." -ForegroundColor Blue
|
||||||
|
|
||||||
|
$releaseUrl = 'https://api.github.com/repos/modernuo/ModernUO/releases/tags/build-tool-latest'
|
||||||
|
$headers = @{ 'User-Agent' = 'ModernUO-BuildTool' }
|
||||||
|
$release = Invoke-RestMethod -Uri $releaseUrl -Headers $headers -TimeoutSec 10
|
||||||
|
|
||||||
|
$asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1
|
||||||
|
if (-not $asset) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$toolsDir = Join-Path $repoRoot 'tools'
|
||||||
|
if (-not (Test-Path $toolsDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $toolsDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $toolBinary -TimeoutSec 60
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-DotNetAvailable {
|
||||||
|
try {
|
||||||
|
$null = & dotnet --version 2>$null
|
||||||
|
return $LASTEXITCODE -eq 0
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try native binary first
|
||||||
|
if (Test-Path $toolBinary) {
|
||||||
|
& $toolBinary @args
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to download native binary
|
||||||
|
if (Get-BuildToolFromRelease) {
|
||||||
|
& $toolBinary @args
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fall back to dotnet run
|
||||||
|
if (Test-DotNetAvailable) {
|
||||||
|
if (Test-Path $buildToolProject) {
|
||||||
|
Write-Host "Using dotnet run fallback..." -ForegroundColor Yellow
|
||||||
|
& dotnet run --project $buildToolProject -- @args
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Error "BuildTool project not found at: $buildToolProject"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nothing works
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Error: Could not run the build tool." -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "The .NET 10 SDK is required. Download it from:" -ForegroundColor Yellow
|
||||||
|
Write-Host " https://dotnet.microsoft.com/download/dotnet/10.0" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
exit 1
|
||||||
114
publish.sh
114
publish.sh
|
|
@ -1,45 +1,93 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
config=$1
|
set -e
|
||||||
os=$2
|
|
||||||
arch=${3:-$(uname -m)}
|
|
||||||
|
|
||||||
if [[ -n $os ]]; then
|
REPO_ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||||
os="-r $os"
|
|
||||||
elif [[ $(uname) = "Darwin" ]]; then
|
# Determine platform for native binary
|
||||||
os="-r osx"
|
detect_platform() {
|
||||||
else
|
local os arch
|
||||||
os="-r linux"
|
case "$(uname -s)" in
|
||||||
|
Darwin) os="osx" ;;
|
||||||
|
*) os="linux" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$(uname -m)" in
|
||||||
|
aarch64|arm64) arch="arm64" ;;
|
||||||
|
*) arch="x64" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "${os}-${arch}"
|
||||||
|
}
|
||||||
|
|
||||||
|
PLATFORM="$(detect_platform)"
|
||||||
|
TOOL_BINARY="$REPO_ROOT/tools/build-tool"
|
||||||
|
BUILD_TOOL_PROJECT="$REPO_ROOT/Projects/BuildTool/BuildTool.csproj"
|
||||||
|
|
||||||
|
# Try to download native binary from GitHub Release
|
||||||
|
download_build_tool() {
|
||||||
|
local asset_name="build-tool-${PLATFORM}"
|
||||||
|
local tools_dir="$REPO_ROOT/tools"
|
||||||
|
|
||||||
|
echo -e "\033[34mDownloading build tool...\033[0m"
|
||||||
|
|
||||||
|
# Get latest release asset URL
|
||||||
|
local release_url="https://api.github.com/repos/modernuo/ModernUO/releases/tags/build-tool-latest"
|
||||||
|
local release_json
|
||||||
|
release_json=$(curl -fsSL --connect-timeout 10 -H "User-Agent: ModernUO-BuildTool" "$release_url" 2>/dev/null) || return 1
|
||||||
|
|
||||||
|
local download_url
|
||||||
|
download_url=$(echo "$release_json" | grep -o "\"browser_download_url\"[[:space:]]*:[[:space:]]*\"[^\"]*${asset_name}[^\"]*\"" | head -1 | grep -o 'https://[^"]*') || return 1
|
||||||
|
|
||||||
|
if [ -z "$download_url" ]; then
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $config ]]; then
|
mkdir -p "$tools_dir"
|
||||||
config="$(tr '[:lower:]' '[:upper:]' <<< ${1:0:1})${1:1}"
|
curl -fsSL --connect-timeout 10 -o "$TOOL_BINARY" "$download_url" || return 1
|
||||||
config="-c $config"
|
chmod +x "$TOOL_BINARY"
|
||||||
else
|
return 0
|
||||||
config="-c Release"
|
}
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $arch == *'aarch'* || $arch == *'arm'* ]]; then
|
has_dotnet() {
|
||||||
arch="arm64"
|
command -v dotnet >/dev/null 2>&1
|
||||||
else
|
}
|
||||||
arch="x64"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $os == *'centos'* || $os == *'rhel'* ]]; then
|
# CentOS/RHEL globalization workaround
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
. /etc/os-release
|
||||||
|
case "$ID" in
|
||||||
|
centos|rhel)
|
||||||
export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo dotnet tool restore
|
# Try native binary first
|
||||||
dotnet tool restore
|
if [ -x "$TOOL_BINARY" ]; then
|
||||||
|
exec "$TOOL_BINARY" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
echo dotnet clean --verbosity quiet
|
# Try to download native binary
|
||||||
dotnet clean --verbosity quiet
|
if download_build_tool 2>/dev/null; then
|
||||||
echo dotnet restore --force-evaluate --source https://api.nuget.org/v3/index.json
|
exec "$TOOL_BINARY" "$@"
|
||||||
dotnet restore --force-evaluate --source https://api.nuget.org/v3/index.json
|
fi
|
||||||
|
|
||||||
echo dotnet publish ${config} ${os}-${arch} --no-restore --self-contained=false
|
# Fall back to dotnet run
|
||||||
dotnet publish ${config} ${os}-${arch} --no-restore --self-contained=false
|
if has_dotnet; then
|
||||||
|
if [ -f "$BUILD_TOOL_PROJECT" ]; then
|
||||||
|
echo -e "\033[33mUsing dotnet run fallback...\033[0m"
|
||||||
|
exec dotnet run --project "$BUILD_TOOL_PROJECT" -- "$@"
|
||||||
|
else
|
||||||
|
echo "Error: BuildTool project not found at: $BUILD_TOOL_PROJECT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo Generating serialization migration schema...
|
# Nothing works
|
||||||
dotnet tool run ModernUOSchemaGenerator -- ModernUO.sln
|
echo ""
|
||||||
|
echo -e "\033[31mError: Could not run the build tool.\033[0m"
|
||||||
exit $?
|
echo ""
|
||||||
|
echo -e "\033[33mThe .NET 10 SDK is required. Download it from:\033[0m"
|
||||||
|
echo -e "\033[36m https://dotnet.microsoft.com/download/dotnet/10.0\033[0m"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
|
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
|
||||||
"version": "0.15.5"
|
"version": "0.15.6"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue