Skip to content

Commit

Permalink
Get a basic software installer up and running
Browse files Browse the repository at this point in the history
  • Loading branch information
rasmus committed Sep 11, 2024
1 parent 60f732c commit acdff80
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// MIT License
//
// Copyright (c) 2021-2024 Rasmus Mikkelsen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using Bake.Services;
using Bake.Tests.Helpers;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;

namespace Bake.Tests.IntegrationTests.ServiceTests
{
public class SoftwareInstallerTests : ServiceTest<SoftwareInstaller>
{
public SoftwareInstallerTests() : base(null) {}

[Test]
public async Task Install()
{
// Arrange
using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(5));

// Act
await Sut.InstallAsync(KnownSoftware.cosign, timeout.Token);
}

protected override IServiceCollection Configure(IServiceCollection serviceCollection)
{
return base.Configure(serviceCollection)
.AddHttpClient();
}
}
}
2 changes: 1 addition & 1 deletion Source/Bake/Core/SemVer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public static SemVer With(
private SemVer(
int major,
int? minor,
int? patch ,
int? patch,
string? meta)
{
Major = major;
Expand Down
1 change: 1 addition & 0 deletions Source/Bake/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public static IServiceCollection AddBake(
.AddSingleton<IGitHubClientFactory, GitHubClientFactory>()
.AddTransient<IPlatformParser, PlatformParser>()
.AddTransient<IMkDocs, MkDocs>()
.AddSingleton<ISoftwareInstaller, SoftwareInstaller>()
.AddTransient<IComposerOrdering, ComposerOrdering>()
.AddTransient<IHelm, Helm>()
.AddTransient<INPM, NPM>()
Expand Down
39 changes: 39 additions & 0 deletions Source/Bake/KnownSoftware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// MIT License
//
// Copyright (c) 2021-2024 Rasmus Mikkelsen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using Bake.Core;
using Bake.ValueObjects;

namespace Bake
{
public static class KnownSoftware
{
public static readonly Software cosign = new(
"cosign",
SemVer.With(2, 4),
new Dictionary<SoftwareDownloadArchitecture, Uri>
{
[SoftwareDownloadArchitecture.LinuxX86] = new ("https://github.com/sigstore/cosign/releases/download/v2.4.0/cosign-linux-amd64"),
[SoftwareDownloadArchitecture.WindowsX86] = new("https://github.com/sigstore/cosign/releases/download/v2.4.0/cosign-windows-amd64.exe"),
});
}
}
33 changes: 33 additions & 0 deletions Source/Bake/Services/ISoftwareInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// MIT License
//
// Copyright (c) 2021-2024 Rasmus Mikkelsen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using Bake.ValueObjects;

namespace Bake.Services
{
public interface ISoftwareInstaller
{
Task<InstalledSoftware> InstallAsync(
Software software,
CancellationToken cancellationToken);
}
}
128 changes: 128 additions & 0 deletions Source/Bake/Services/SoftwareInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// MIT License
//
// Copyright (c) 2021-2024 Rasmus Mikkelsen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using Bake.ValueObjects;
using Microsoft.Extensions.Logging;

namespace Bake.Services
{
public class SoftwareInstaller : ISoftwareInstaller
{
private readonly ILogger<SoftwareInstaller> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ConcurrentDictionary<string, Lazy<Task<InstalledSoftware>>> _alreadyInstalledSoftware = new();

public SoftwareInstaller(
ILogger<SoftwareInstaller> logger,
IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
}

public Task<InstalledSoftware> InstallAsync(
Software software,
CancellationToken cancellationToken)
{
return _alreadyInstalledSoftware.GetOrAdd(
CreateCacheKey(software),
_ => new Lazy<Task<InstalledSoftware>>(
() => InstallInternalAsync(software, cancellationToken),
LazyThreadSafetyMode.ExecutionAndPublication)).Value;
}

private async Task<InstalledSoftware> InstallInternalAsync(
Software software,
CancellationToken cancellationToken)
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(TimeSpan.FromMinutes(5));

var directory = Path.Combine(
Path.GetTempPath(),
$"bake-software-{Guid.NewGuid():N}");
var softwareDownloadArchitecture = PickArchitecture();
var filename = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"{software.Name}.exe" : software.Name;
var filepath = Path.Combine(directory, filename);

_logger.LogInformation($"Installing {software.Name} v{software.Version} for {softwareDownloadArchitecture} to temp directory {directory}");

if (!software.Downloads.TryGetValue(softwareDownloadArchitecture, out var url))
{
throw new InvalidOperationException($"No download URL found for {software.Name} v{software.Version} for {softwareDownloadArchitecture}");
}

var httpClient = _httpClientFactory.CreateClient();
{
using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, timeout.Token);
response.EnsureSuccessStatusCode(); // TODO: Do something more readable

await using var filestream = File.OpenWrite(filename);
await using var stream = await response.Content.ReadAsStreamAsync(timeout.Token);
await stream.CopyToAsync(filestream, timeout.Token);
}

return new InstalledSoftware(
software,
filepath);
}

private static SoftwareDownloadArchitecture PickArchitecture()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
switch (RuntimeInformation.OSArchitecture)
{
case Architecture.X64:
case Architecture.X86:
return SoftwareDownloadArchitecture.WindowsX86;
default:
throw new NotSupportedException(
$"Unsupported OS architecture {RuntimeInformation.OSArchitecture}");

}
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
switch (RuntimeInformation.OSArchitecture)
{
case Architecture.X64:
case Architecture.X86:
return SoftwareDownloadArchitecture.LinuxX86;
default:
throw new NotSupportedException($"Unsupported OS architecture {RuntimeInformation.OSArchitecture}");
}

}

throw new NotSupportedException($"Unsupported OS {RuntimeInformation.OSDescription}");
}

private static string CreateCacheKey(Software software)
{
return $"{software.Name} v{software.Version}";
}
}
}
38 changes: 38 additions & 0 deletions Source/Bake/ValueObjects/InstalledSoftware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// MIT License
//
// Copyright (c) 2021-2024 Rasmus Mikkelsen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

namespace Bake.ValueObjects
{
public class InstalledSoftware
{
public Software Software { get; }
public string Path { get; }

public InstalledSoftware(
Software software,
string path)
{
Software = software;
Path = path;
}
}
}
48 changes: 48 additions & 0 deletions Source/Bake/ValueObjects/Software.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// MIT License
//
// Copyright (c) 2021-2024 Rasmus Mikkelsen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using Bake.Core;

namespace Bake.ValueObjects
{
public class Software
{
public string Name { get; }
public SemVer Version { get; }
public IReadOnlyDictionary<SoftwareDownloadArchitecture, Uri> Downloads { get; }

public Software(
string name,
SemVer version,
IReadOnlyDictionary<SoftwareDownloadArchitecture, Uri> downloads)
{
Name = name;
Version = version;
Downloads = downloads;
}

public override string ToString()
{
return $"{Name} v{Version}";
}
}
}
30 changes: 30 additions & 0 deletions Source/Bake/ValueObjects/SoftwareDownloadArchitecture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// MIT License
//
// Copyright (c) 2021-2024 Rasmus Mikkelsen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

namespace Bake.ValueObjects
{
public enum SoftwareDownloadArchitecture
{
LinuxX86,
WindowsX86
}
}

0 comments on commit acdff80

Please sign in to comment.