-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add macOS support #50
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -87,3 +87,6 @@ packages | |
|
||
# Windows | ||
Thumbs.db | ||
|
||
# macOS | ||
.DS_Store |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
namespace Cupboard | ||
{ | ||
public abstract class AsyncMacResourceProvider<TResource> : AsyncResourceProvider<TResource> | ||
where TResource : Resource | ||
{ | ||
public override bool CanRun(FactCollection facts) | ||
{ | ||
return facts.IsMacOS(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using Spectre.IO; | ||
|
||
namespace Cupboard | ||
{ | ||
public sealed class BashScript : Resource | ||
{ | ||
public BashScript(string name) | ||
: base(name) | ||
{ | ||
} | ||
|
||
public FilePath? Script { get; set; } | ||
public string? Command { get; set; } | ||
public string? Unless { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
using Spectre.IO; | ||
|
||
namespace Cupboard | ||
{ | ||
public static class BashScriptExtensions | ||
{ | ||
public static IResourceBuilder<BashScript> Script(this IResourceBuilder<BashScript> builder, FilePath file) | ||
{ | ||
builder.Configure(res => res.Script = file); | ||
return builder; | ||
} | ||
|
||
public static IResourceBuilder<BashScript> Command(this IResourceBuilder<BashScript> builder, string command) | ||
{ | ||
builder.Configure(res => res.Command = command); | ||
return builder; | ||
} | ||
|
||
public static IResourceBuilder<BashScript> Unless(this IResourceBuilder<BashScript> builder, string script) | ||
{ | ||
builder.Configure(res => res.Unless = script); | ||
return builder; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
using CliWrap; | ||
using CliWrap.EventStream; | ||
using Spectre.IO; | ||
|
||
namespace Cupboard | ||
{ | ||
public sealed class BashScriptProvider : AsyncMacResourceProvider<BashScript> | ||
{ | ||
private readonly ICupboardFileSystem _fileSystem; | ||
private readonly IEnvironment _environment; | ||
private readonly IEnvironmentRefresher _refresher; | ||
private readonly ICupboardLogger _logger; | ||
|
||
public BashScriptProvider( | ||
ICupboardFileSystem fileSystem, | ||
IEnvironment environment, | ||
IEnvironmentRefresher refresher, | ||
ICupboardLogger logger) | ||
{ | ||
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); | ||
_environment = environment ?? throw new ArgumentNullException(nameof(environment)); | ||
_refresher = refresher ?? throw new ArgumentNullException(nameof(refresher)); | ||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||
} | ||
|
||
public override BashScript Create(string name) | ||
{ | ||
return new BashScript(name); | ||
} | ||
|
||
public override async Task<ResourceState> RunAsync(IExecutionContext context, BashScript resource) | ||
{ | ||
if (resource.Script == null) | ||
{ | ||
_logger.Error("Script path has not been set"); | ||
return ResourceState.Error; | ||
} | ||
|
||
var path = resource.Script.MakeAbsolute(_environment); | ||
if (!_fileSystem.Exist(path)) | ||
{ | ||
_logger.Error("Script path does not exist"); | ||
return ResourceState.Error; | ||
} | ||
|
||
if (resource.Unless != null) | ||
{ | ||
_logger.Debug("Evaluating condition"); | ||
if (await RunBash(resource.Unless).ConfigureAwait(false) != 0) | ||
{ | ||
return ResourceState.Skipped; | ||
} | ||
} | ||
|
||
if (!context.DryRun) | ||
{ | ||
var content = await GetScriptContent(path).ConfigureAwait(false); | ||
if (await RunBash(content).ConfigureAwait(false) != 0) | ||
{ | ||
_logger.Error("Bash script exited with an unexpected exit code"); | ||
return ResourceState.Error; | ||
} | ||
|
||
_logger.Debug("Refreshing environment variables for user"); | ||
_refresher.Refresh(); | ||
} | ||
|
||
return ResourceState.Unchanged; | ||
} | ||
|
||
private async Task<string> GetScriptContent(FilePath path) | ||
{ | ||
if (path is null) | ||
{ | ||
throw new ArgumentNullException(nameof(path)); | ||
} | ||
|
||
await using var stream = _fileSystem.File.OpenRead(path); | ||
using var reader = new StreamReader(stream); | ||
return await reader.ReadToEndAsync().ConfigureAwait(false); | ||
} | ||
|
||
private async Task<int> RunBash(string content) | ||
{ | ||
// Get temporary location | ||
var path = new FilePath(System.IO.Path.GetTempFileName()).ChangeExtension("sh"); | ||
await using (var stream = _fileSystem.File.OpenWrite(path)) | ||
await using (var writer = new StreamWriter(stream)) | ||
{ | ||
writer.Write(content); | ||
} | ||
|
||
try | ||
{ | ||
// Create file with content | ||
var result = Cli.Wrap("/bin/zsh") | ||
.WithValidation(CommandResultValidation.None); | ||
|
||
var first = true; | ||
await foreach (var cmdEvent in result.ListenAsync()) | ||
{ | ||
switch (cmdEvent) | ||
{ | ||
case StandardOutputCommandEvent stdOut: | ||
if (first) | ||
{ | ||
first = false; | ||
_logger.Verbose("--------------------------------"); | ||
} | ||
|
||
if (!string.IsNullOrWhiteSpace(stdOut.Text)) | ||
{ | ||
_logger.Verbose(stdOut.Text); | ||
} | ||
|
||
break; | ||
case StandardErrorCommandEvent stdErr: | ||
if (first) | ||
{ | ||
first = false; | ||
_logger.Verbose("--------------------------------"); | ||
} | ||
|
||
_logger.Error(stdErr.Text); | ||
break; | ||
case ExitedCommandEvent exited: | ||
if (!first) | ||
{ | ||
_logger.Verbose("--------------------------------"); | ||
} | ||
|
||
return exited.ExitCode; | ||
} | ||
} | ||
|
||
throw new InvalidOperationException("An error occured while executing Bash script"); | ||
} | ||
finally | ||
{ | ||
_fileSystem.File.Delete(path); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<AdditionalFiles Include="..\stylecop.json" Link="Properties\stylecop.json" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\Cupboard.Core\Cupboard.Core.csproj" /> | ||
<ProjectReference Include="..\Cupboard.Providers\Cupboard.Providers.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | ||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=homebrew/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might be better as an editorconfig 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that how it's currently implemented and I missed it? Likely just me clicking buttons too fast during development trying to remove compiler warnings. I can remove this file |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
namespace Cupboard | ||
{ | ||
public sealed class HomebrewPackage : Resource, IHasPackageState, IHasPackageName | ||
{ | ||
public string Package { get; set; } | ||
public bool IsCask { get; set; } | ||
public PackageState Ensure { get; set; } | ||
public bool PreRelease { get; set; } | ||
public bool IgnoreChecksum { get; set; } | ||
public bool AllowDowngrade { get; set; } | ||
public string? PackageParameters { get; set; } | ||
public string? PackageVersion { get; set; } | ||
|
||
public string Install() => $"install {(IsCask ? "--cask" : string.Empty)} {Package}"; | ||
public string List() => $"ls {Package}"; | ||
|
||
public HomebrewPackage(string name) | ||
: base(name) | ||
{ | ||
Package = name ?? throw new ArgumentNullException(nameof(name)); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
namespace Cupboard | ||
{ | ||
public static class BrewPackageExtensions | ||
{ | ||
public static IResourceBuilder<HomebrewPackage> Ensure(this IResourceBuilder<HomebrewPackage> builder, PackageState state) | ||
{ | ||
return builder.Configure(pkg => pkg.Ensure = state); | ||
} | ||
|
||
public static IResourceBuilder<HomebrewPackage> Package(this IResourceBuilder<HomebrewPackage> builder, string package) | ||
{ | ||
return builder.Configure(pkg => pkg.Package = package); | ||
} | ||
|
||
public static IResourceBuilder<HomebrewPackage> IncludePreRelease(this IResourceBuilder<HomebrewPackage> builder) | ||
{ | ||
return builder.Configure(pkg => pkg.PreRelease = true); | ||
} | ||
|
||
public static IResourceBuilder<HomebrewPackage> IgnoreChecksum(this IResourceBuilder<HomebrewPackage> builder) | ||
{ | ||
return builder.Configure(pkg => pkg.IgnoreChecksum = true); | ||
} | ||
|
||
public static IResourceBuilder<HomebrewPackage> PackageParameters(this IResourceBuilder<HomebrewPackage> builder, string packageParameters) | ||
{ | ||
return builder.Configure(pkg => pkg.PackageParameters = packageParameters); | ||
} | ||
|
||
public static IResourceBuilder<HomebrewPackage> UseVersion(this IResourceBuilder<HomebrewPackage> builder, string version) | ||
{ | ||
return builder.Configure(pkg => pkg.PackageVersion = version); | ||
} | ||
|
||
public static IResourceBuilder<HomebrewPackage> AllowDowngrade(this IResourceBuilder<HomebrewPackage> builder) | ||
{ | ||
return builder.Configure(pkg => pkg.AllowDowngrade = true); | ||
} | ||
|
||
public static IResourceBuilder<HomebrewPackage> IsCask(this IResourceBuilder<HomebrewPackage> builder) => | ||
builder.Configure(pkg => pkg.IsCask = true); | ||
|
||
public static IResourceBuilder<HomebrewPackage> OnError(this IResourceBuilder<HomebrewPackage> builder, ErrorOptions handle) => | ||
builder.Configure(pkg => pkg.Error = handle); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bash Script is not exclusive to Mac 🤔
Windows has mingw bash and wsl bash.
Linux has bash :)
However, noted that you specified the CLI wrap for /bin/zsh which is default for mac.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. It doesn't belong to windows either. So I am open to suggestions on the taxonomy. I put it here because I was adding macOS support and it's the default on mac as you stated.
Likely need some guidance from @patriksvensson on where he'd like it to live and be pulled into macOS.