diff --git a/.github/workflows/build-on-main.yaml b/.github/workflows/build-on-main.yaml index c5678eb..097fa13 100644 --- a/.github/workflows/build-on-main.yaml +++ b/.github/workflows/build-on-main.yaml @@ -13,15 +13,23 @@ permissions: jobs: unit-test: - runs-on: ubuntu-latest - + strategy: + matrix: + os: + - [self-hosted, large, jammy, X64] + - [self-hosted, large, jammy, ARM64] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + id: checkout - name: Install .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' + id: install-dotnet + run: | + sudo apt-get update + sudo apt-get upgrade --yes + sudo apt-get install --yes dotnet-sdk-8.0 + echo "DPKG_ARCH=$(dpkg --print-architecture)" >> "$GITHUB_OUTPUT" - name: Run Tests run: dotnet test --verbosity normal --logger trx --results-directory TestResults @@ -31,16 +39,25 @@ jobs: if: always() with: check_name: test-results - comment_title: Test Results + comment_title: Test Results (${{ steps.install-dotnet.outputs.DPKG_ARCH }}) files: | TestResults/*.trx snapcraft-build: - runs-on: ubuntu-latest + strategy: + matrix: + os: + - [self-hosted, large, jammy, X64] + - [self-hosted, large, jammy, ARM64] + runs-on: ${{ matrix.os }} needs: unit-test - steps: - uses: actions/checkout@v4 + id: checkout + + - name: Retrieve Architecture + id: get-arch + run: echo "DPKG_ARCH=$(dpkg --print-architecture)" >> "$GITHUB_OUTPUT" - name: Run Snapcraft id: snapcraft @@ -50,19 +67,23 @@ jobs: id: upload-artifact uses: actions/upload-artifact@v4 with: - name: snap + name: dotnet-${{ steps.get-arch.outputs.DPKG_ARCH }} path: ${{ steps.snapcraft.outputs.snap }} snapcraft-publish: runs-on: ubuntu-latest if: ${{ contains(fromJSON('["push", "workflow_dispatch"]'), github.event_name) && github.ref_name == 'main' }} needs: snapcraft-build - + strategy: + matrix: + artifact-name: + - dotnet-amd64 + - dotnet-arm64 steps: - uses: actions/download-artifact@v4 id: download-artifact with: - name: snap + name: ${{ matrix.artifact-name }} - name: Gather filename id: gather-filename @@ -70,7 +91,7 @@ jobs: ARTIFACT_PATH: ${{ steps.download-artifact.outputs.download-path }} run: | ls -la $ARTIFACT_PATH - SNAP_FILE_NAME=$(ls ${ARTIFACT_PATH}/${SNAP_NAME}*.snap) + SNAP_FILE_NAME=$(ls ${ARTIFACT_PATH}/dotnet*.snap) echo "SNAP_PATH=${SNAP_FILE_NAME}" >> "$GITHUB_OUTPUT" - uses: snapcore/action-publish@v1 @@ -85,14 +106,16 @@ jobs: if: ${{ github.event_name == 'push' }} permissions: contents: write - steps: - uses: actions/checkout@v4 + id: checkout - name: Install .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' + id: install-dotnet + run: | + sudo apt-get update + sudo apt-get upgrade --yes + sudo apt-get install --yes dotnet-sdk-8.0 - name: Install reportgenerator tool run: | diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3969021..cdbc09b 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -16,9 +16,15 @@ grade: devel # must be 'stable' to release into candidate/stable channels confinement: classic package-repositories: - - type: apt - ppa: dotnet/snaps - priority: always + - type: apt + ppa: dotnet/snaps + priority: always + +architectures: + - build-on: [amd64] + build-for: [amd64] + - build-on: [arm64] + build-for: [arm64] parts: pre-reqs: @@ -39,6 +45,7 @@ parts: dotnet-installer: plugin: dump source: . + after: [ pre-reqs ] build-packages: - dotnet-sdk-8.0 stage-packages: @@ -53,30 +60,39 @@ parts: elif [ "${CRAFT_ARCH_BUILD_FOR}" = "arm64" ]; then RUNTIME_RID="linux-arm64" else - echo "Unknown architecture (${CRAFT_ARCH_BUILD_FOR})" + echo "Unsupported architecture (${CRAFT_ARCH_BUILD_FOR})" exit 1 fi /usr/bin/dotnet publish src/Dotnet.Installer.Console --output "${SNAPCRAFT_PART_INSTALL}" \ --configuration Release -r "${RUNTIME_RID}" -p:DebugSymbols=false -p:DebugType=none chmod 555 "${SNAPCRAFT_PART_INSTALL}/Dotnet.Installer.Console" - - craftctl default override-stage: | craftctl default - # jq files - patchelf --set-interpreter /snap/dotnet/current/lib64/ld-linux-x86-64.so.2 usr/bin/jq - patchelf --force-rpath --set-rpath /snap/dotnet/current/usr/lib/x86_64-linux-gnu usr/bin/jq - # .NET Installer files - patchelf --set-interpreter /snap/dotnet/current/lib64/ld-linux-x86-64.so.2 Dotnet.Installer.Console - patchelf --force-rpath --set-rpath \$ORIGIN/netcoredeps:/snap/dotnet/current/usr/lib/x86_64-linux-gnu Dotnet.Installer.Console - prime: - - usr/bin/jq - - usr/lib/*/libjq.so* - - usr/lib/*/libonig.so* - - Dotnet.Installer.Console - - Configuration/* - - Scripts/* + + if [ "${CRAFT_ARCH_BUILD_FOR}" = "amd64" ]; then + LIB_PATH=lib/x86_64-linux-gnu + INTERPRETER="/snap/dotnet/current/lib64/ld-linux-x86-64.so.2" + + patchelf --force-rpath --set-rpath \$ORIGIN/../${LIB_PATH} \ + ${CRAFT_STAGE}/usr/bin/jq + patchelf --force-rpath --set-rpath \$ORIGIN/usr/${LIB_PATH} \ + ${CRAFT_STAGE}/Dotnet.Installer.Console + elif [ "${CRAFT_ARCH_BUILD_FOR}" = "arm64" ]; then + LIB_PATH=lib/aarch64-linux-gnu + INTERPRETER="/snap/dotnet/current/lib/ld-linux-aarch64.so.1" + + patchelf --force-rpath --set-rpath \$ORIGIN/../${LIB_PATH}:\$ORIGIN/../../${LIB_PATH} \ + ${CRAFT_STAGE}/usr/bin/jq + patchelf --force-rpath --set-rpath \$ORIGIN/${LIB_PATH}:\$ORIGIN/usr/${LIB_PATH} \ + ${CRAFT_STAGE}/Dotnet.Installer.Console + else + echo "Unsupported architecture (${CRAFT_ARCH_BUILD_FOR})" + exit 1 + fi + + patchelf --set-interpreter $INTERPRETER ${CRAFT_STAGE}/usr/bin/jq + patchelf --set-interpreter $INTERPRETER ${CRAFT_STAGE}/Dotnet.Installer.Console dotnet-host: plugin: nil @@ -86,10 +102,22 @@ parts: - dotnet-hostfxr-8.0 override-stage: | craftctl default + + if [ "${CRAFT_ARCH_BUILD_FOR}" = "amd64" ]; then + INTERPRETER="/snap/dotnet/current/lib64/ld-linux-x86-64.so.2" + RPATH="/snap/dotnet/current/usr/lib/x86_64-linux-gnu" + elif [ "${CRAFT_ARCH_BUILD_FOR}" = "arm64" ]; then + INTERPRETER="/snap/dotnet/current/lib/ld-linux-aarch64.so.1" + RPATH="/snap/dotnet/current/usr/lib/aarch64-linux-gnu:/snap/dotnet/current/lib/aarch64-linux-gnu" + else + echo "Unsupported architecture (${CRAFT_ARCH_BUILD_FOR})" + exit 1 + fi + DOTNET_VERSION=$(craftctl get version) - patchelf --set-interpreter /snap/dotnet/current/lib64/ld-linux-x86-64.so.2 usr/lib/dotnet/dotnet - patchelf --force-rpath --set-rpath /snap/dotnet/current/usr/lib/x86_64-linux-gnu usr/lib/dotnet/dotnet - patchelf --force-rpath --set-rpath /snap/dotnet/current/usr/lib/x86_64-linux-gnu usr/lib/dotnet/host/fxr/${DOTNET_VERSION}/libhostfxr.so + patchelf --set-interpreter $INTERPRETER usr/lib/dotnet/dotnet + patchelf --force-rpath --set-rpath $RPATH usr/lib/dotnet/dotnet + patchelf --force-rpath --set-rpath $RPATH usr/lib/dotnet/host/fxr/${DOTNET_VERSION}/libhostfxr.so netstandard-targeting-pack: plugin: nil diff --git a/src/Dotnet.Installer.Core/Services/Contracts/ISnapService.cs b/src/Dotnet.Installer.Core/Services/Contracts/ISnapService.cs index ee4453c..4abb69e 100644 --- a/src/Dotnet.Installer.Core/Services/Contracts/ISnapService.cs +++ b/src/Dotnet.Installer.Core/Services/Contracts/ISnapService.cs @@ -5,6 +5,6 @@ namespace Dotnet.Installer.Core.Services.Contracts; public interface ISnapService { bool IsSnapInstalled(string name, CancellationToken cancellationToken = default); - Task Install(string name, CancellationToken cancellationToken = default); - Task Remove(string name, bool purge = false, CancellationToken cancellationToken = default); -} \ No newline at end of file + Task Install(string name, CancellationToken cancellationToken = default); + Task Remove(string name, bool purge = false, CancellationToken cancellationToken = default); +} diff --git a/src/Dotnet.Installer.Core/Services/Contracts/ISystemdService.cs b/src/Dotnet.Installer.Core/Services/Contracts/ISystemdService.cs index 59a6593..916ed99 100644 --- a/src/Dotnet.Installer.Core/Services/Contracts/ISystemdService.cs +++ b/src/Dotnet.Installer.Core/Services/Contracts/ISystemdService.cs @@ -4,9 +4,9 @@ namespace Dotnet.Installer.Core.Services.Contracts; public interface ISystemdService { - Task DaemonReload(); - Task EnableUnit(string unit); - Task DisableUnit(string unit); - Task StartUnit(string unit); - Task StopUnit(string unit); + Task DaemonReload(); + Task EnableUnit(string unit); + Task DisableUnit(string unit); + Task StartUnit(string unit); + Task StopUnit(string unit); } diff --git a/src/Dotnet.Installer.Core/Services/Implementations/SnapService.cs b/src/Dotnet.Installer.Core/Services/Implementations/SnapService.cs index 508408d..a5b585e 100644 --- a/src/Dotnet.Installer.Core/Services/Implementations/SnapService.cs +++ b/src/Dotnet.Installer.Core/Services/Implementations/SnapService.cs @@ -10,23 +10,21 @@ public bool IsSnapInstalled(string name, CancellationToken cancellationToken = d return Directory.Exists(Path.Combine("/", "snap", name)); } - public async Task Install(string name, CancellationToken cancellationToken = default) + public Task Install(string name, CancellationToken cancellationToken = default) { - var result = await Terminal.Invoke("snap", "install", name); - return new InvocationResult(result == 0, "", ""); + return Terminal.Invoke("snap", "install", name); } - public async Task Remove(string name, bool purge = false, CancellationToken cancellationToken = default) + public Task Remove(string name, bool purge = false, CancellationToken cancellationToken = default) { var arguments = new List { "remove" }; - + if (purge) arguments.Add("--purge"); arguments.Add(name); - - var result = await Terminal.Invoke("snap", arguments.ToArray()); - return new InvocationResult(result == 0, "", ""); + + return Terminal.Invoke("snap", arguments.ToArray()); } -} \ No newline at end of file +} diff --git a/src/Dotnet.Installer.Core/Services/Implementations/SystemdService.cs b/src/Dotnet.Installer.Core/Services/Implementations/SystemdService.cs index 4ca920c..c396684 100644 --- a/src/Dotnet.Installer.Core/Services/Implementations/SystemdService.cs +++ b/src/Dotnet.Installer.Core/Services/Implementations/SystemdService.cs @@ -10,34 +10,29 @@ public class SystemdService : ISystemdService RedirectStandardError = true, RedirectStandardOutput = true, }; - - public async Task DaemonReload() + + public Task DaemonReload() { - var result = await Terminal.Invoke("systemctl", _globalSystemdOptions, "daemon-reload"); - return new InvocationResult(result == 0, string.Empty, string.Empty); + return Terminal.Invoke("systemctl", _globalSystemdOptions, "daemon-reload"); } - public async Task EnableUnit(string unit) + public Task EnableUnit(string unit) { - var result = await Terminal.Invoke("systemctl", _globalSystemdOptions, "enable", unit); - return new InvocationResult(result == 0, string.Empty, string.Empty); + return Terminal.Invoke("systemctl", _globalSystemdOptions, "enable", unit); } - public async Task DisableUnit(string unit) + public Task DisableUnit(string unit) { - var result = await Terminal.Invoke("systemctl", _globalSystemdOptions, "disable", unit); - return new InvocationResult(result == 0, string.Empty, string.Empty); + return Terminal.Invoke("systemctl", _globalSystemdOptions, "disable", unit); } - public async Task StartUnit(string unit) + public Task StartUnit(string unit) { - var result = await Terminal.Invoke("systemctl", _globalSystemdOptions, "start", unit); - return new InvocationResult(result == 0, string.Empty, string.Empty); + return Terminal.Invoke("systemctl", _globalSystemdOptions, "start", unit); } - public async Task StopUnit(string unit) + public Task StopUnit(string unit) { - var result = await Terminal.Invoke("systemctl", _globalSystemdOptions, "stop", unit); - return new InvocationResult(result == 0, string.Empty, string.Empty); + return Terminal.Invoke("systemctl", _globalSystemdOptions, "stop", unit); } } diff --git a/src/Dotnet.Installer.Core/Types/InvocationResult.cs b/src/Dotnet.Installer.Core/Types/InvocationResult.cs deleted file mode 100644 index 93a2c2a..0000000 --- a/src/Dotnet.Installer.Core/Types/InvocationResult.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Dotnet.Installer.Core.Types; - -public class InvocationResult(bool isSuccess, string standardOutput, string standardError) -{ - public bool IsSuccess { get; init; } = isSuccess; - public string StandardOutput { get; init; } = standardOutput; - public string StandardError { get; init; } = standardError; -} \ No newline at end of file diff --git a/src/Dotnet.Installer.Core/Types/Terminal.cs b/src/Dotnet.Installer.Core/Types/Terminal.cs index 25c6fac..28ec614 100644 --- a/src/Dotnet.Installer.Core/Types/Terminal.cs +++ b/src/Dotnet.Installer.Core/Types/Terminal.cs @@ -4,12 +4,12 @@ namespace Dotnet.Installer.Core.Types; public static class Terminal { - public static async Task Invoke(string program, params string[] arguments) + public static async Task Invoke(string program, params string[] arguments) { return await Invoke(program, options: null, arguments); } - public static async Task Invoke(string program, InvocationOptions? options = default, + public static async Task Invoke(string program, InvocationOptions? options = default, params string[] arguments) { options ??= InvocationOptions.Default; @@ -28,7 +28,16 @@ public static async Task Invoke(string program, InvocationOptions? options process.Start(); await process.WaitForExitAsync(); - return process.ExitCode; + return new InvocationResult + { + ExitCode = process.ExitCode, + RedirectedStandardError = options.RedirectStandardError, + RedirectedStandardOutput = options.RedirectStandardOutput, + StandardError = options.RedirectStandardError ? + await process.StandardError.ReadToEndAsync() : default, + StandardOutput = options.RedirectStandardOutput ? + await process.StandardOutput.ReadToEndAsync() : default + }; } public class InvocationOptions @@ -38,4 +47,27 @@ public class InvocationOptions public bool RedirectStandardOutput { get; set; } = false; public bool RedirectStandardError { get; set; } = false; } + + public class InvocationResult + { + public InvocationResult() + { } + + public InvocationResult(int exitCode, string standardOutput, string standardError) + { + ExitCode = exitCode; + StandardOutput = standardOutput; + StandardError = standardError; + + RedirectedStandardError = true; + RedirectedStandardOutput = true; + } + + public bool IsSuccess => ExitCode == 0; + public int ExitCode { get; init; } + public bool RedirectedStandardOutput { get; init; } + public bool RedirectedStandardError { get; init; } + public string? StandardOutput { get; init; } + public string? StandardError { get; init; } + } } diff --git a/tests/Dotnet.Installer.Core.Tests/Models/ComponentTests.cs b/tests/Dotnet.Installer.Core.Tests/Models/ComponentTests.cs index 1e18cd0..516f80e 100644 --- a/tests/Dotnet.Installer.Core.Tests/Models/ComponentTests.cs +++ b/tests/Dotnet.Installer.Core.Tests/Models/ComponentTests.cs @@ -29,15 +29,15 @@ public async Task Install_WithValidVersions_ShouldInvokeInstallationStartedEvent var systemDService = new Mock(); snapService.Setup(s => s.Install(It.IsAny(), CancellationToken.None)) - .ReturnsAsync(new InvocationResult( - isSuccess: true, standardOutput: string.Empty, standardError: string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult( + exitCode: 0, standardOutput: string.Empty, standardError: string.Empty)); systemDService.Setup(s => s.DaemonReload()) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); systemDService.Setup(s => s.EnableUnit(It.IsAny())) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); systemDService.Setup(s => s.StartUnit(It.IsAny())) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); // Act var evt = await Assert.RaisesAsync( @@ -73,15 +73,15 @@ public async Task Install_WithValidVersions_ShouldInvokeInstallationFinishedEven var systemDService = new Mock(); snapService.Setup(s => s.Install(It.IsAny(), CancellationToken.None)) - .ReturnsAsync(new InvocationResult( - isSuccess: true, standardOutput: string.Empty, standardError: string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult( + exitCode: 0, standardOutput: string.Empty, standardError: string.Empty)); systemDService.Setup(s => s.DaemonReload()) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); systemDService.Setup(s => s.EnableUnit(It.IsAny())) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); systemDService.Setup(s => s.StartUnit(It.IsAny())) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); // Act var evt = await Assert.RaisesAsync( @@ -146,15 +146,15 @@ public async Task Install_WithMultipleDependencies_ShouldTraverseAndInstallDepen }); snapService.Setup(s => s.Install(It.IsAny(), CancellationToken.None)) - .ReturnsAsync(new InvocationResult( - isSuccess: true, standardOutput: string.Empty, standardError: string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult( + exitCode: 0, standardOutput: string.Empty, standardError: string.Empty)); systemDService.Setup(s => s.DaemonReload()) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); systemDService.Setup(s => s.EnableUnit(It.IsAny())) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); systemDService.Setup(s => s.StartUnit(It.IsAny())) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); // Act await component1.Install(fileService.Object, manifestService.Object, snapService.Object, systemDService.Object, @@ -201,11 +201,11 @@ public async Task Uninstall_WithInstalledComponent_ShouldUninstall() }); systemDService.Setup(s => s.DaemonReload()) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); systemDService.Setup(s => s.DisableUnit(It.IsAny())) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); systemDService.Setup(s => s.StopUnit(It.IsAny())) - .ReturnsAsync(new InvocationResult(true, string.Empty, string.Empty)); + .ReturnsAsync(new Terminal.InvocationResult(0, string.Empty, string.Empty)); installedComponents.Add(component1);