Skip to content
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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ packages

# Windows
Thumbs.db

# macOS
.DS_Store
11 changes: 11 additions & 0 deletions src/Cupboard.Providers.MacOs/AsyncMacResourceProvider.cs
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();
}
}
}
16 changes: 16 additions & 0 deletions src/Cupboard.Providers.MacOs/Bash/BashScript.cs
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; }
}
}
25 changes: 25 additions & 0 deletions src/Cupboard.Providers.MacOs/Bash/BashScriptExtensions.cs
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;
}
}
}
144 changes: 144 additions & 0 deletions src/Cupboard.Providers.MacOs/Bash/BashScriptProvider.cs
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>
Copy link
Contributor

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.

Copy link
Author

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.

{
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);
}
}
}
}
18 changes: 18 additions & 0 deletions src/Cupboard.Providers.MacOs/Cupboard.Providers.MacOs.csproj
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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be better as an editorconfig 🤔

Copy link
Author

Choose a reason for hiding this comment

The 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

23 changes: 23 additions & 0 deletions src/Cupboard.Providers.MacOs/Homebrew/HomebrewPackage.cs
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));
}
}
}
46 changes: 46 additions & 0 deletions src/Cupboard.Providers.MacOs/Homebrew/HomebrewPackageExtensions.cs
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);
}
}
Loading