├── global.json ├── src ├── Cupboard.Core │ ├── IHasPackageName.cs │ ├── IEnvironmentRefresher.cs │ ├── IRebootDetector.cs │ ├── ErrorOptions.cs │ ├── IHasPackageState.cs │ ├── IReportSubscriber.cs │ ├── ISecurityPrincipal.cs │ ├── PackageState.cs │ ├── IO │ │ ├── ChmodFormatting.cs │ │ ├── ICupboardFileSystem.cs │ │ ├── ChmodClass.cs │ │ ├── ICupboardEnvironment.cs │ │ ├── Permissions.cs │ │ ├── SpecialMode.cs │ │ ├── IProcessRunner.cs │ │ ├── RegistryHive.cs │ │ ├── RegistryValueKind.cs │ │ ├── Obsolete │ │ │ ├── RegistryKeyRoot.cs │ │ │ └── RegistryKeyValueKind.cs │ │ ├── IWindowsRegistry.cs │ │ ├── ProcessRunnerResult.cs │ │ ├── IWindowsRegistryKey.cs │ │ ├── ChmodParser.cs │ │ ├── ChmodFormatter.cs │ │ ├── Chmod.cs │ │ └── RegistryPath.cs │ ├── RebootOptions.cs │ ├── Manifest.cs │ ├── PackageInstallerResult.cs │ ├── IFactBuilder.cs │ ├── Verbosity.cs │ ├── PackageInstallerOperation.cs │ ├── IResourceIdentity.cs │ ├── ResourceState.cs │ ├── ServiceModule.cs │ ├── IFactProvider.cs │ ├── Catalog.cs │ ├── ICupboardLogger.cs │ ├── IResourceBuilder.cs │ ├── Extensions │ │ ├── IEnumerableExtensions.cs │ │ ├── FileSystemExtensions.cs │ │ ├── ResourceStateExtensions.cs │ │ ├── PathExtensions.cs │ │ ├── ResourceExtensions.cs │ │ ├── IWindowsRegistryExtensions.cs │ │ └── ICupboardLoggerExtensions.cs │ ├── IResourceProvider.cs │ ├── IExecutionContext.cs │ ├── Resource.cs │ ├── ReportItem.cs │ ├── CatalogContext.cs │ ├── Cupboard.Core.csproj │ ├── ManifestContext.cs │ ├── LogLevel.cs │ ├── Report.cs │ ├── ResourceProvider.cs │ ├── ResourceBuilder.cs │ ├── FactCollection.cs │ └── Fact.cs ├── Cupboard.Providers │ ├── File │ │ ├── FileState.cs │ │ ├── File.cs │ │ └── FileExtensions.cs │ ├── Directory │ │ ├── DirectoryState.cs │ │ ├── Directory.cs │ │ ├── DirectoryExtensions.cs │ │ └── DirectoryProvider.cs │ ├── PowerShell │ │ ├── PowerShellFlavor.cs │ │ ├── PowerShell.cs │ │ └── PowerShellScriptExtensions.cs │ ├── Exec │ │ ├── Exec.cs │ │ ├── ExecExtensions.cs │ │ └── ExecProvider.cs │ ├── VSCode │ │ ├── VSCodeExtension.cs │ │ ├── VSCodeExtensionExtensions.cs │ │ └── VSCodeExtensionProvider.cs │ ├── ArgumentFacts.cs │ ├── Cupboard.Providers.csproj │ ├── Download │ │ ├── Download.cs │ │ └── DownloadExtensions.cs │ ├── EnvironmentFacts.cs │ ├── ResourcesModule.cs │ └── MachineFacts.cs ├── Cupboard.Providers.Windows │ ├── Registry │ │ ├── RegistryKeyState.cs │ │ ├── RegistryValue.cs │ │ ├── Obsolete │ │ │ ├── RegistryKey.cs │ │ │ └── RegistryKeyExtensions.cs │ │ └── RegistryValueExtensions.cs │ ├── Features │ │ ├── WindowsFeatureState.cs │ │ ├── WindowsFeature.cs │ │ └── WindowsFeatureExtensions.cs │ ├── WindowsFacts.cs │ ├── Winget │ │ ├── WingetPackage.cs │ │ ├── WingetPackageExtensions.cs │ │ └── WingetPackageProvider.cs │ ├── WindowsResourceProvider.cs │ ├── Chocolatey │ │ ├── ChocolateyPackage.cs │ │ ├── ChocolateyPackageExtensions.cs │ │ └── ChocolateyPackageProvider.cs │ ├── Cupboard.Providers.Windows.csproj │ ├── WindowsModule.cs │ └── WmiFacts.cs ├── Cupboard.Testing │ ├── Fakes │ │ ├── FakeEnvironmentRefresher.cs │ │ ├── FakeSecurityPrincipal.cs │ │ ├── FakeRebootDetector.cs │ │ ├── FakeReportSubscriber.cs │ │ ├── FakeFactBuilder.cs │ │ ├── FakeWindowsRegistry.cs │ │ ├── FakeCupboardFileSystem.cs │ │ ├── FakeWindowsRegistryKey.cs │ │ ├── FakeCupboardEnvironment.cs │ │ ├── FakeLogger.cs │ │ └── FakeProcessRunner.cs │ ├── LambdaCatalog.cs │ ├── Cupboard.Testing.csproj │ └── Extensions │ │ ├── ReportExtensions.cs │ │ └── CupboardFixtureExtensions.cs ├── Cupboard │ ├── WindowsCatalog.cs │ ├── ResourceGraphEdge.cs │ ├── Extensions │ │ ├── IAnsiConsoleExtensions.cs │ │ ├── RegistryValueKindExtensions.cs │ │ ├── RegistryKeyValueKindExtensions.cs │ │ └── ServiceCollectionExtensions.cs │ ├── IO │ │ ├── CupboardFileSystem.cs │ │ ├── WindowsRegistry.cs │ │ ├── CupboardEnvironment.cs │ │ ├── WindowsRegistryKey.cs │ │ └── ProcessRunner.cs │ ├── SecurityPrincipal.cs │ ├── Cli │ │ ├── Infrastructure │ │ │ ├── TypeResolver.cs │ │ │ ├── FilePathConverter.cs │ │ │ ├── DirectoryPathConverter.cs │ │ │ ├── TypeRegistrar.cs │ │ │ └── VerbosityConverter.cs │ │ └── FactCommand.cs │ ├── ExecutionPlanItem.cs │ ├── ResourceIdentity.cs │ ├── FactBuilder.cs │ ├── IStatusUpdater.cs │ ├── ExecutionPlan.cs │ ├── Cupboard.csproj │ ├── ResourceProviderRepository.cs │ ├── ResourceComparer.cs │ ├── ColorPalette.cs │ ├── CupboardHost.cs │ ├── EnvironmentRefresher.cs │ ├── ResourceGraphBuilder.cs │ ├── CupboardLogger.cs │ ├── ResourceGraph.cs │ ├── ExecutionPlanBuilder.cs │ ├── RebootDetector.cs │ ├── CupboardHostBuilder.cs │ └── ResourceGraphWalker.cs ├── Directory.Build.targets ├── Sandbox │ ├── Manifests │ │ ├── WingetPackages.cs │ │ ├── ChocolateyPackages.cs │ │ ├── Chocolatey.cs │ │ ├── VSCode.cs │ │ ├── Rust.cs │ │ └── WindowsSettings.cs │ ├── Sandbox.csproj │ ├── Program.cs │ └── Facts │ │ └── RustFactProvider.cs ├── Cupboard.Tests │ ├── FakeProcessRunnerFixture.cs │ ├── Properties │ │ └── WindowsFact.cs │ ├── Cupboard.Tests.csproj │ ├── .editorconfig │ ├── FactCollectionTests.cs │ └── Unit │ │ ├── IO │ │ ├── RegistryKeyTests.cs │ │ └── ChmodParserTests.cs │ │ └── Providers │ │ └── FileProviderTests.cs ├── stylecop.json ├── Directory.Build.props └── .editorconfig ├── dotnet-tools.json ├── LICENSE.md ├── .github └── workflows │ ├── ci.yaml │ └── publish.yaml ├── .gitignore └── CODE_OF_CONDUCT.md /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src" ], 3 | "sdk": { 4 | "version": "5.0.301", 5 | "rollForward": "latestPatch" 6 | } 7 | } -------------------------------------------------------------------------------- /src/Cupboard.Core/IHasPackageName.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public interface IHasPackageName 4 | { 5 | string Package { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/File/FileState.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum FileState 4 | { 5 | Present, 6 | Absent, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IEnvironmentRefresher.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public interface IEnvironmentRefresher 4 | { 5 | void Refresh(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IRebootDetector.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public interface IRebootDetector 4 | { 5 | bool HasPendingReboot(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Cupboard.Core/ErrorOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum ErrorOptions 4 | { 5 | IgnoreErrors = 0, 6 | Abort = 1, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IHasPackageState.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public interface IHasPackageState 4 | { 5 | PackageState Ensure { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IReportSubscriber.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public interface IReportSubscriber 4 | { 5 | void Notify(Report report); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Cupboard.Core/ISecurityPrincipal.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public interface ISecurityPrincipal 4 | { 5 | bool IsAdministrator(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Cupboard.Core/PackageState.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum PackageState 4 | { 5 | Installed = 1, 6 | Uninstalled = 2, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/ChmodFormatting.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum ChmodFormatting 4 | { 5 | Numeric = 0, 6 | Symbolic = 1, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/ICupboardFileSystem.cs: -------------------------------------------------------------------------------- 1 | using Spectre.IO; 2 | 3 | namespace Cupboard 4 | { 5 | public interface ICupboardFileSystem : IFileSystem 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cupboard.Core/RebootOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum RebootOptions 4 | { 5 | IgnorePendingReboot = 0, 6 | Reboot = 1, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Manifest.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public abstract class Manifest 4 | { 5 | public abstract void Execute(ManifestContext context); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/Directory/DirectoryState.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum DirectoryState 4 | { 5 | Present = 0, 6 | Absent = 1, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/PowerShell/PowerShellFlavor.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum PowerShellFlavor 4 | { 5 | PowerShell, 6 | PowerShellCore, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/ChmodClass.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum ChmodClass 4 | { 5 | Owner = 0, 6 | Group = 1, 7 | Other = 2, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Registry/RegistryKeyState.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum RegistryKeyState 4 | { 5 | Exist = 0, 6 | DoNotExist = 1, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Features/WindowsFeatureState.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum WindowsFeatureState 4 | { 5 | Enabled = 0, 6 | Disabled, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cupboard.Core/PackageInstallerResult.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum PackageInstallerResult 4 | { 5 | Exists = 0, 6 | Missing = 1, 7 | Error = 2, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "cake.tool": { 6 | "version": "1.1.0", 7 | "commands": [ 8 | "dotnet-cake" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Cupboard.Core/IFactBuilder.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console.Cli; 2 | 3 | namespace Cupboard 4 | { 5 | public interface IFactBuilder 6 | { 7 | FactCollection Build(IRemainingArguments args); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/ICupboardEnvironment.cs: -------------------------------------------------------------------------------- 1 | using Spectre.IO; 2 | 3 | namespace Cupboard 4 | { 5 | public interface ICupboardEnvironment : IEnvironment 6 | { 7 | FilePath GetTempFilePath(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Verbosity.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum Verbosity 4 | { 5 | Quiet = 0, 6 | Minimal, 7 | Normal, 8 | Verbose, 9 | Diagnostic, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Cupboard.Core/PackageInstallerOperation.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum PackageInstallerOperation 4 | { 5 | RetriveState = 0, 6 | Install = 1, 7 | Uninstall = 2, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IResourceIdentity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | public interface IResourceIdentity 6 | { 7 | Type ResourceType { get; } 8 | 9 | string Name { get; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Cupboard.Core/ResourceState.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum ResourceState 4 | { 5 | Unknown = 0, 6 | Changed, 7 | Unchanged, 8 | Executed, 9 | Skipped, 10 | Error, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeEnvironmentRefresher.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard.Testing 2 | { 3 | public sealed class FakeEnvironmentRefresher : IEnvironmentRefresher 4 | { 5 | public void Refresh() 6 | { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Cupboard.Core/ServiceModule.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Cupboard 4 | { 5 | public abstract class ServiceModule 6 | { 7 | public abstract void Configure(IServiceCollection services); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Cupboard/WindowsCatalog.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public abstract class WindowsCatalog : Catalog 4 | { 5 | public override bool CanRun(FactCollection facts) 6 | { 7 | return facts.IsWindows(); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IFactProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Spectre.Console.Cli; 3 | 4 | namespace Cupboard 5 | { 6 | public interface IFactProvider 7 | { 8 | IEnumerable<(string Name, object Value)> GetFacts(IRemainingArguments args); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/Permissions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | [Flags] 6 | public enum Permissions 7 | { 8 | None = 0, 9 | Execute = 1, 10 | Write = 2, 11 | Read = 4, 12 | All = Read | Write | Execute, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | preview 5 | normal 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Catalog.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public abstract class Catalog 4 | { 5 | public virtual bool CanRun(FactCollection facts) 6 | { 7 | return true; 8 | } 9 | 10 | public abstract void Execute(CatalogContext context); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/SpecialMode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | [Flags] 6 | public enum SpecialMode 7 | { 8 | None = 0, 9 | Sticky = 1, 10 | Setgid = 1 << 1, 11 | Setuid = 1 << 2, 12 | All = Sticky | Setgid | Setuid, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/IProcessRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Cupboard 5 | { 6 | public interface IProcessRunner 7 | { 8 | Task Run(string file, string arguments, Func? filter = null, bool supressOutput = false); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/RegistryHive.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum RegistryHive 4 | { 5 | Unknown = 0, 6 | ClassesRoot = 1, 7 | CurrentUser = 2, 8 | LocalMachine = 3, 9 | Users = 4, 10 | CurrentConfig = 5, 11 | PerformanceData = 6, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/RegistryValueKind.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum RegistryValueKind 4 | { 5 | None = -1, 6 | Unknown = 0, 7 | String = 1, 8 | ExpandString = 2, 9 | Binary = 3, 10 | DWord = 4, 11 | MultiString = 7, 12 | QWord = 11, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeSecurityPrincipal.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard.Testing 2 | { 3 | public sealed class FakeSecurityPrincipal : ISecurityPrincipal 4 | { 5 | public bool IsAdmin { get; set; } 6 | 7 | public bool IsAdministrator() 8 | { 9 | return IsAdmin; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeRebootDetector.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard.Testing 2 | { 3 | public sealed class FakeRebootDetector : IRebootDetector 4 | { 5 | public bool PendingReboot { get; set; } 6 | 7 | public bool HasPendingReboot() 8 | { 9 | return PendingReboot; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeReportSubscriber.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard.Testing 2 | { 3 | internal sealed class FakeReportSubscriber : IReportSubscriber 4 | { 5 | public Report? Report { get; set; } 6 | 7 | public void Notify(Report report) 8 | { 9 | Report = report; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Cupboard.Core/ICupboardLogger.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public interface ICupboardLogger 4 | { 5 | Verbosity Verbosity { get; } 6 | 7 | void SetVerbosity(Verbosity verbosity); 8 | void Log(Verbosity verbosity, LogLevel level, string text); 9 | void Log(Verbosity verbosity, LogLevel level, string title, string text); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/Obsolete/RegistryKeyRoot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | [Obsolete("Please use RegistryRoot instead")] 6 | public enum RegistryKeyRoot 7 | { 8 | Unknown = 0, 9 | ClassesRoot = 1, 10 | CurrentUser = 2, 11 | LocalMachine = 3, 12 | Users = 4, 13 | CurrentConfig = 5, 14 | PerformanceData = 6, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Features/WindowsFeature.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public sealed class WindowsFeature : Resource 4 | { 5 | public string? FeatureName { get; set; } 6 | public WindowsFeatureState Ensure { get; set; } = WindowsFeatureState.Enabled; 7 | 8 | public WindowsFeature(string name) 9 | : base(name) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Cupboard/ResourceGraphEdge.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard.Internal 2 | { 3 | internal sealed class ResourceGraphEdge 4 | { 5 | public IResourceIdentity From { get; } 6 | public IResourceIdentity To { get; } 7 | 8 | public ResourceGraphEdge(IResourceIdentity from, IResourceIdentity to) 9 | { 10 | From = from; 11 | To = to; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IResourceBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | public interface IResourceBuilder 6 | where TResource : Resource 7 | { 8 | IResourceBuilder Before(string name); 9 | IResourceBuilder After(string name); 10 | IResourceBuilder Configure(Action action); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/Obsolete/RegistryKeyValueKind.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | [Obsolete("Please use RegistryValueKind instead")] 6 | public enum RegistryKeyValueKind 7 | { 8 | None = -1, 9 | Unknown = 0, 10 | String = 1, 11 | ExpandString = 2, 12 | Binary = 3, 13 | DWord = 4, 14 | MultiString = 7, 15 | QWord = 11, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Extensions/IEnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard 5 | { 6 | public static class IEnumerableExtensions 7 | { 8 | public static IReadOnlyList ToReadOnlyList(this IEnumerable source) 9 | { 10 | return source as IReadOnlyList 11 | ?? new List(source ?? Array.Empty()); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/IWindowsRegistry.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public interface IWindowsRegistry 4 | { 5 | IWindowsRegistryKey ClassesRoot { get; } 6 | IWindowsRegistryKey CurrentConfig { get; } 7 | IWindowsRegistryKey CurrentUser { get; } 8 | IWindowsRegistryKey LocalMachine { get; } 9 | IWindowsRegistryKey PerformanceData { get; } 10 | IWindowsRegistryKey Users { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IResourceProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Cupboard 5 | { 6 | public interface IResourceProvider 7 | { 8 | Type ResourceType { get; } 9 | 10 | Resource Create(string name); 11 | 12 | bool RequireAdministrator(FactCollection facts); 13 | bool CanRun(FactCollection facts); 14 | Task RunAsync(IExecutionContext context, Resource resource); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Sandbox/Manifests/WingetPackages.cs: -------------------------------------------------------------------------------- 1 | using Cupboard; 2 | 3 | namespace Sandbox 4 | { 5 | public sealed class WingetPackages : Manifest 6 | { 7 | public override void Execute(ManifestContext context) 8 | { 9 | foreach (var package in new[] { "GitHub.cli" }) 10 | { 11 | context.Resource(package) 12 | .Ensure(PackageState.Installed); 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeFactBuilder.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console.Cli; 2 | 3 | namespace Cupboard.Testing 4 | { 5 | public sealed class FakeFactBuilder : IFactBuilder 6 | { 7 | public FactCollection Facts { get; set; } 8 | 9 | public FakeFactBuilder() 10 | { 11 | Facts = new FactCollection(); 12 | } 13 | 14 | public FactCollection Build(IRemainingArguments args) 15 | { 16 | return Facts; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Cupboard/Extensions/IAnsiConsoleExtensions.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console; 2 | 3 | namespace Cupboard.Internal 4 | { 5 | internal static class IAnsiConsoleExtensions 6 | { 7 | public static bool Confirm(this IAnsiConsole console, string markup, bool defaultValue = true) 8 | { 9 | return new ConfirmationPrompt(markup) 10 | { 11 | DefaultValue = defaultValue, 12 | } 13 | .Show(console); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/Directory/Directory.cs: -------------------------------------------------------------------------------- 1 | using Spectre.IO; 2 | 3 | namespace Cupboard 4 | { 5 | public sealed class Directory : Resource 6 | { 7 | public DirectoryPath Path { get; set; } 8 | public DirectoryState Ensure { get; set; } = DirectoryState.Present; 9 | public Chmod? Permissions { get; set; } 10 | 11 | public Directory(string name) 12 | : base(name) 13 | { 14 | Path = new DirectoryPath(name); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Cupboard/IO/CupboardFileSystem.cs: -------------------------------------------------------------------------------- 1 | using Spectre.IO; 2 | 3 | namespace Cupboard.Internal 4 | { 5 | internal sealed class CupboardFileSystem : ICupboardFileSystem 6 | { 7 | private readonly IFileSystem _fileSystem; 8 | 9 | public IFileProvider File => _fileSystem.File; 10 | public IDirectoryProvider Directory => _fileSystem.Directory; 11 | 12 | public CupboardFileSystem() 13 | { 14 | _fileSystem = new FileSystem(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/Exec/Exec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Spectre.IO; 3 | 4 | namespace Cupboard 5 | { 6 | public sealed class Exec : Resource 7 | { 8 | public FilePath Path { get; set; } 9 | public string? Args { get; set; } 10 | public int[]? ValidExitCodes { get; set; } 11 | 12 | public Exec(string name) 13 | : base(name) 14 | { 15 | Path = new FilePath(name ?? throw new ArgumentNullException(nameof(name))); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/PowerShell/PowerShell.cs: -------------------------------------------------------------------------------- 1 | using Spectre.IO; 2 | 3 | namespace Cupboard 4 | { 5 | public sealed class PowerShell : Resource 6 | { 7 | public FilePath? Script { get; set; } 8 | public string? Command { get; set; } 9 | public string? Unless { get; set; } 10 | public PowerShellFlavor Flavor { get; set; } = PowerShellFlavor.PowerShell; 11 | 12 | public PowerShell(string name) 13 | : base(name) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Sandbox/Sandbox.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | false 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/VSCode/VSCodeExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | public sealed class VSCodeExtension : Resource, IHasPackageName, IHasPackageState 6 | { 7 | public string Package { get; set; } 8 | public PackageState Ensure { get; set; } = PackageState.Installed; 9 | 10 | public VSCodeExtension(string name) 11 | : base(name) 12 | { 13 | Package = name ?? throw new ArgumentNullException(nameof(name)); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/File/File.cs: -------------------------------------------------------------------------------- 1 | using Spectre.IO; 2 | 3 | namespace Cupboard 4 | { 5 | public sealed class File : Resource 6 | { 7 | public FilePath? Destination { get; set; } 8 | public FilePath? Source { get; set; } 9 | public FileState Ensure { get; set; } = FileState.Present; 10 | public bool SymbolicLink { get; set; } 11 | public Chmod? Permissions { get; set; } 12 | 13 | public File(string name) 14 | : base(name) 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/ArgumentFacts.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Spectre.Console.Cli; 4 | 5 | namespace Cupboard 6 | { 7 | internal sealed class ArgumentFacts : IFactProvider 8 | { 9 | public IEnumerable<(string Name, object Value)> GetFacts(IRemainingArguments args) 10 | { 11 | foreach (var argument in args.Parsed) 12 | { 13 | yield return ("arg." + argument.Key, argument.Last() ?? string.Empty); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/LambdaCatalog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard.Testing 4 | { 5 | public sealed class LambdaCatalog : Catalog 6 | { 7 | private readonly Action _action; 8 | 9 | public LambdaCatalog(Action action) 10 | { 11 | _action = action ?? throw new ArgumentNullException(nameof(action)); 12 | } 13 | 14 | public override void Execute(CatalogContext context) 15 | { 16 | _action(context); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/WindowsFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Spectre.Console.Cli; 4 | 5 | namespace Cupboard 6 | { 7 | internal sealed class WindowsFacts : IFactProvider 8 | { 9 | public IEnumerable<(string Name, object Value)> GetFacts(IRemainingArguments args) 10 | { 11 | var isSandboxUser = Environment.UserName.Equals("WDAGUtilityAccount", StringComparison.OrdinalIgnoreCase); 12 | yield return ("windows.sandbox", isSandboxUser); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Sandbox/Manifests/ChocolateyPackages.cs: -------------------------------------------------------------------------------- 1 | using Cupboard; 2 | 3 | namespace Sandbox 4 | { 5 | public sealed class ChocolateyPackages : Manifest 6 | { 7 | public override void Execute(ManifestContext context) 8 | { 9 | foreach (var package in new[] { "screentogif", "repoz" }) 10 | { 11 | context.Resource(package) 12 | .Ensure(PackageState.Installed) 13 | .After("Install Chocolatey"); 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Cupboard.Providers/Cupboard.Providers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 9.0 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/ProcessRunnerResult.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public sealed class ProcessRunnerResult 4 | { 5 | public int ExitCode { get; } 6 | public string StandardOut { get; } 7 | public string StandardError { get; } 8 | 9 | public ProcessRunnerResult(int exitCode, string? standardOut = null, string? standardError = null) 10 | { 11 | ExitCode = exitCode; 12 | StandardOut = standardOut ?? string.Empty; 13 | StandardError = standardError ?? string.Empty; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Registry/RegistryValue.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public sealed class RegistryValue : Resource 4 | { 5 | public RegistryPath? Path { get; set; } 6 | public string? Value { get; set; } 7 | public object? Data { get; set; } 8 | public RegistryValueKind ValueKind { get; set; } = RegistryValueKind.Unknown; 9 | public RegistryKeyState State { get; set; } = RegistryKeyState.Exist; 10 | 11 | public RegistryValue(string name) 12 | : base(name) 13 | { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/Download/Download.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Spectre.IO; 3 | 4 | namespace Cupboard 5 | { 6 | public sealed class Download : Resource 7 | { 8 | public Uri? Url { get; set; } 9 | public Chmod? Permissions { get; set; } 10 | public Path? Destination { get; set; } 11 | 12 | public Download(string name) 13 | : base(name) 14 | { 15 | if (Uri.TryCreate(name, UriKind.Absolute, out var result)) 16 | { 17 | Url = result; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Extensions/FileSystemExtensions.cs: -------------------------------------------------------------------------------- 1 | using Spectre.IO; 2 | 3 | namespace Cupboard 4 | { 5 | public static class FileSystemExtensions 6 | { 7 | public static bool CreateSymbolicLinkSafe(this IFileProvider fileProvider, FilePath source, FilePath destination) 8 | { 9 | try 10 | { 11 | fileProvider.CreateSymbolicLink(source, destination); 12 | return true; 13 | } 14 | catch 15 | { 16 | return false; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IExecutionContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | public interface IExecutionContext 6 | { 7 | FactCollection Facts { get; } 8 | bool DryRun { get; } 9 | } 10 | 11 | public sealed class ExecutionContext : IExecutionContext 12 | { 13 | public FactCollection Facts { get; } 14 | public bool DryRun { get; init; } = false; 15 | 16 | public ExecutionContext(FactCollection facts) 17 | { 18 | Facts = facts ?? throw new ArgumentNullException(nameof(facts)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Cupboard/SecurityPrincipal.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Security.Principal; 3 | using Mono.Unix.Native; 4 | 5 | namespace Cupboard.Internal 6 | { 7 | internal sealed class SecurityPrincipal : ISecurityPrincipal 8 | { 9 | public bool IsAdministrator() 10 | { 11 | return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 12 | new WindowsPrincipal(WindowsIdentity.GetCurrent()) 13 | .IsInRole(WindowsBuiltInRole.Administrator) : 14 | Syscall.geteuid() == 0; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Winget/WingetPackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | public sealed class WingetPackage : Resource, IHasPackageName, IHasPackageState 6 | { 7 | public string Package { get; set; } 8 | public PackageState Ensure { get; set; } 9 | public bool Force { get; set; } 10 | public string? PackageVersion { get; set; } 11 | 12 | public WingetPackage(string name) 13 | : base(name) 14 | { 15 | Package = name ?? throw new ArgumentNullException(nameof(name)); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Sandbox/Program.cs: -------------------------------------------------------------------------------- 1 | using Cupboard; 2 | 3 | namespace Sandbox 4 | { 5 | public static class Program 6 | { 7 | public static int Main(string[] args) 8 | { 9 | return CupboardHost.CreateBuilder() 10 | .AddCatalog() 11 | .Run(args); 12 | } 13 | } 14 | 15 | public sealed class SandboxCatalog : WindowsCatalog 16 | { 17 | public override void Execute(CatalogContext context) 18 | { 19 | context.UseManifest(); 20 | context.UseManifest(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Resource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | public abstract class Resource : IResourceIdentity 6 | { 7 | public Type ResourceType => GetType(); 8 | 9 | public string Name { get; } 10 | public bool RequireAdministrator { get; set; } 11 | 12 | public ErrorOptions Error { get; set; } = ErrorOptions.Abort; 13 | public RebootOptions Reboot { get; set; } = RebootOptions.Reboot; 14 | 15 | protected Resource(string name) 16 | { 17 | Name = name ?? throw new ArgumentNullException(nameof(name)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Registry/Obsolete/RegistryKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | [Obsolete("Please use the RegistryValue resource")] 6 | public sealed class RegistryKey : Resource 7 | { 8 | public RegistryKeyPath? Path { get; set; } 9 | public object? Value { get; set; } 10 | public RegistryKeyValueKind ValueKind { get; set; } = RegistryKeyValueKind.Unknown; 11 | public RegistryKeyState State { get; set; } = RegistryKeyState.Exist; 12 | 13 | public RegistryKey(string name) 14 | : base(name) 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/VSCode/VSCodeExtensionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public static class VSCodeExtensionExtensions 4 | { 5 | public static IResourceBuilder Ensure(this IResourceBuilder builder, PackageState state) 6 | { 7 | return builder.Configure(file => file.Ensure = state); 8 | } 9 | 10 | public static IResourceBuilder Package(this IResourceBuilder builder, string package) 11 | { 12 | return builder.Configure(pkg => pkg.Package = package); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Cupboard.Tests/FakeProcessRunnerFixture.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | 3 | namespace Cupboard.Tests.Resources 4 | { 5 | public sealed class FakeProcessRunnerFixture 6 | { 7 | public IProcessRunner Runner { get; } 8 | 9 | public FakeProcessRunnerFixture() 10 | { 11 | Runner = Substitute.For(); 12 | } 13 | 14 | public void Register(string file, string arguments, ProcessRunnerResult result, params ProcessRunnerResult[] results) 15 | { 16 | Runner.Run(Arg.Is(file), Arg.Is(arguments)).Returns(result, results); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Cupboard.Tests/Properties/WindowsFact.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using Xunit; 3 | 4 | namespace Cupboard.Tests.Unit 5 | { 6 | public sealed class WindowsFact : FactAttribute 7 | { 8 | private static readonly bool _isWindows; 9 | 10 | static WindowsFact() 11 | { 12 | _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 13 | } 14 | 15 | public WindowsFact(string reason = null) 16 | { 17 | if (!_isWindows) 18 | { 19 | Skip = reason ?? "Windows test."; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Cupboard.Core/ReportItem.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public sealed class ReportItem 4 | { 5 | public Resource Resource { get; } 6 | public IResourceProvider Provider { get; } 7 | public ResourceState State { get; } 8 | public bool RequireAdministrator { get; } 9 | 10 | public ReportItem(IResourceProvider provider, Resource resource, ResourceState state, bool requireAdministrator) 11 | { 12 | Provider = provider; 13 | Resource = resource; 14 | State = state; 15 | RequireAdministrator = requireAdministrator; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Features/WindowsFeatureExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public static class WindowsFeatureExtensions 4 | { 5 | public static IResourceBuilder Ensure(this IResourceBuilder builder, WindowsFeatureState state) 6 | { 7 | return builder.Configure(file => file.Ensure = state); 8 | } 9 | 10 | public static IResourceBuilder FeatureName(this IResourceBuilder builder, string name) 11 | { 12 | return builder.Configure(file => file.FeatureName = name); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Cupboard/Cli/Infrastructure/TypeResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Spectre.Console.Cli; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | internal sealed class TypeResolver : ITypeResolver 7 | { 8 | private readonly IServiceProvider _provider; 9 | 10 | public TypeResolver(IServiceProvider provider) 11 | { 12 | _provider = provider; 13 | } 14 | 15 | public object? Resolve(Type? type) 16 | { 17 | if (type == null) 18 | { 19 | return null; 20 | } 21 | 22 | return _provider.GetService(type); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/WindowsResourceProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public abstract class WindowsResourceProvider : ResourceProvider 4 | where TResource : Resource 5 | { 6 | public override bool CanRun(FactCollection facts) 7 | { 8 | return facts.IsWindows(); 9 | } 10 | } 11 | 12 | public abstract class AsyncWindowsResourceProvider : AsyncResourceProvider 13 | where TResource : Resource 14 | { 15 | public override bool CanRun(FactCollection facts) 16 | { 17 | return facts.IsWindows(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Chocolatey/ChocolateyPackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | public sealed class ChocolateyPackage : Resource, IHasPackageState, IHasPackageName 6 | { 7 | public string Package { get; set; } 8 | public PackageState Ensure { get; set; } 9 | public bool PreRelease { get; set; } 10 | public bool IgnoreChecksum { get; set; } 11 | public string? PackageParameters { get; set; } 12 | 13 | public ChocolateyPackage(string name) 14 | : base(name) 15 | { 16 | Package = name ?? throw new ArgumentNullException(nameof(name)); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Cupboard/ExecutionPlanItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard.Internal 4 | { 5 | internal sealed class ExecutionPlanItem 6 | { 7 | public IResourceProvider Provider { get; } 8 | public Resource Resource { get; } 9 | public bool RequireAdministrator { get; } 10 | 11 | public ExecutionPlanItem(IResourceProvider provider, Resource resource, bool requireAdministrator) 12 | { 13 | Provider = provider ?? throw new ArgumentNullException(nameof(provider)); 14 | Resource = resource ?? throw new ArgumentNullException(nameof(resource)); 15 | RequireAdministrator = requireAdministrator; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Cupboard.Testing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 9.0 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/IWindowsRegistryKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | public interface IWindowsRegistryKey 6 | { 7 | IWindowsRegistryKey? OpenSubKey(string name, bool writable); 8 | IWindowsRegistryKey? CreateSubKey(string name, bool writable); 9 | 10 | int GetValueCount(); 11 | 12 | bool ValueExists(string name); 13 | object? GetValue(string name); 14 | void DeleteValue(string name); 15 | 16 | [Obsolete("Please use SetValue overload accepting a RegistryValueKind instead")] 17 | void SetValue(string name, object value, RegistryKeyValueKind kind); 18 | 19 | void SetValue(string name, object value, RegistryValueKind kind); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Extensions/ResourceStateExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | public static class ResourceStateExtensions 6 | { 7 | public static bool IsError(this ResourceState state) 8 | { 9 | return state switch 10 | { 11 | ResourceState.Unknown => true, 12 | ResourceState.Changed => false, 13 | ResourceState.Unchanged => false, 14 | ResourceState.Error => true, 15 | ResourceState.Skipped => false, 16 | ResourceState.Executed => false, 17 | _ => throw new InvalidOperationException($"Unknown resource state '{state}'"), 18 | }; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Extensions/ReportExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Cupboard.Testing 5 | { 6 | public static class ReportExtensions 7 | { 8 | public static ResourceState GetState(this Report report, string name) 9 | where TResource : Resource 10 | { 11 | if (report == null) 12 | { 13 | return ResourceState.Unknown; 14 | } 15 | 16 | var item = report.Items.SingleOrDefault( 17 | x => x.Resource.GetType() == typeof(TResource) 18 | && x.Resource.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); 19 | 20 | return item?.State ?? ResourceState.Unknown; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Cupboard/Cli/Infrastructure/FilePathConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | using Spectre.IO; 5 | 6 | namespace Cupboard.Internal 7 | { 8 | /// 9 | /// A type converter for . 10 | /// 11 | internal sealed class FilePathConverter : TypeConverter 12 | { 13 | /// 14 | public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) 15 | { 16 | if (value is string stringValue) 17 | { 18 | return new FilePath(stringValue); 19 | } 20 | 21 | throw new NotSupportedException("Can't convert value to file path."); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Extensions/PathExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Mono.Unix; 4 | using Spectre.IO; 5 | 6 | namespace Cupboard 7 | { 8 | public static class PathExtensions 9 | { 10 | public static void SetPermissions(this Path path, Chmod chmod) 11 | { 12 | if (path is null) 13 | { 14 | throw new ArgumentNullException(nameof(path)); 15 | } 16 | 17 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 18 | { 19 | return; 20 | } 21 | 22 | var info = UnixFileSystemInfo.GetFileSystemEntry(path.FullPath); 23 | info.FileAccessPermissions = chmod.ToFileAccessPermissions(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Cupboard/ResourceIdentity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard.Internal 4 | { 5 | internal sealed class ResourceIdentity : IResourceIdentity 6 | { 7 | public Type ResourceType { get; } 8 | public string Name { get; } 9 | 10 | public ResourceIdentity(Type resourceType, string name) 11 | { 12 | ResourceType = resourceType; 13 | Name = name; 14 | } 15 | 16 | public ResourceIdentity(Resource resource) 17 | { 18 | ResourceType = resource.ResourceType; 19 | Name = resource.Name; 20 | } 21 | 22 | public ResourceIdentity(Tuple tuple) 23 | { 24 | ResourceType = tuple.Item1; 25 | Name = tuple.Item2; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Cupboard/Cli/Infrastructure/DirectoryPathConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | using Spectre.IO; 5 | 6 | namespace Cupboard.Internal 7 | { 8 | /// 9 | /// A type converter for . 10 | /// 11 | internal sealed class DirectoryPathConverter : TypeConverter 12 | { 13 | /// 14 | public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) 15 | { 16 | if (value is string stringValue) 17 | { 18 | return new DirectoryPath(stringValue); 19 | } 20 | 21 | throw new NotSupportedException("Can't convert value to file path."); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Cupboard.Core/CatalogContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard 5 | { 6 | public sealed class CatalogContext 7 | { 8 | private readonly HashSet _manifests; 9 | 10 | public FactCollection Facts { get; } 11 | 12 | public CatalogContext(FactCollection facts) 13 | { 14 | Facts = facts ?? throw new ArgumentNullException(nameof(facts)); 15 | _manifests = new HashSet(); 16 | } 17 | 18 | public void UseManifest() 19 | where TManifest : Manifest 20 | { 21 | _manifests.Add(typeof(TManifest)); 22 | } 23 | 24 | public IEnumerable GetManifests() 25 | { 26 | return _manifests; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/EnvironmentFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Spectre.Console.Cli; 4 | 5 | namespace Cupboard 6 | { 7 | internal sealed class EnvironmentFacts : IFactProvider 8 | { 9 | private readonly ICupboardEnvironment _environment; 10 | 11 | public EnvironmentFacts(ICupboardEnvironment environment) 12 | { 13 | _environment = environment ?? throw new ArgumentNullException(nameof(environment)); 14 | } 15 | 16 | public IEnumerable<(string Name, object Value)> GetFacts(IRemainingArguments args) 17 | { 18 | foreach (var (key, value) in _environment.GetEnvironmentVariables()) 19 | { 20 | yield return ($"env.{key}", value ?? string.Empty); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "documentExposedElements": true, 6 | "documentInternalElements": false, 7 | "documentPrivateElements": false, 8 | "documentPrivateFields": false 9 | }, 10 | "layoutRules": { 11 | "newlineAtEndOfFile": "allow", 12 | "allowConsecutiveUsings": true 13 | }, 14 | "orderingRules": { 15 | "usingDirectivesPlacement": "outsideNamespace", 16 | "systemUsingDirectivesFirst": true, 17 | "elementOrder": [ 18 | "kind", 19 | "accessibility", 20 | "constant", 21 | "static", 22 | "readonly" 23 | ] 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Cupboard.Core/Cupboard.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 9.0 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Cupboard.Providers.Windows.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 9.0 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/Exec/ExecExtensions.cs: -------------------------------------------------------------------------------- 1 | using Spectre.IO; 2 | 3 | namespace Cupboard 4 | { 5 | public static class ExecExtensions 6 | { 7 | public static IResourceBuilder Path(this IResourceBuilder builder, FilePath file) 8 | { 9 | builder.Configure(res => res.Path = file); 10 | return builder; 11 | } 12 | 13 | public static IResourceBuilder Arguments(this IResourceBuilder builder, string args) 14 | { 15 | builder.Configure(res => res.Args = args); 16 | return builder; 17 | } 18 | 19 | public static IResourceBuilder ValidExitCodes(this IResourceBuilder builder, params int[] exitCodes) 20 | { 21 | builder.Configure(res => res.ValidExitCodes = exitCodes); 22 | return builder; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Cupboard.Core/ManifestContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard 5 | { 6 | public sealed class ManifestContext 7 | { 8 | public FactCollection Facts { get; } 9 | public List Builders { get; } 10 | 11 | public ManifestContext(FactCollection facts) 12 | { 13 | Facts = facts ?? throw new ArgumentNullException(nameof(facts)); 14 | Builders = new List(); 15 | } 16 | 17 | public IResourceBuilder Resource(string name) 18 | where TResource : Resource 19 | { 20 | var builder = new ResourceBuilder(name); 21 | 22 | // Store a reference to the configuration. 23 | Builders.Add(builder); 24 | 25 | return builder; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/Directory/DirectoryExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public static class DirectoryExtensions 4 | { 5 | public static IResourceBuilder Ensure(this IResourceBuilder builder, DirectoryState state) 6 | { 7 | return builder.Configure(directory => directory.Ensure = state); 8 | } 9 | 10 | public static IResourceBuilder Permissions(this IResourceBuilder builder, string chmod) 11 | { 12 | return builder.Configure(directory => directory.Permissions = ChmodParser.Parse(chmod)); 13 | } 14 | 15 | public static IResourceBuilder Permissions(this IResourceBuilder builder, Chmod permissions) 16 | { 17 | return builder.Configure(directory => directory.Permissions = permissions); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Cupboard/FactBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Spectre.Console.Cli; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | internal sealed class FactBuilder : IFactBuilder 7 | { 8 | private readonly IReadOnlyList _providers; 9 | 10 | public FactBuilder(IEnumerable providers) 11 | { 12 | _providers = providers.ToReadOnlyList(); 13 | } 14 | 15 | public FactCollection Build(IRemainingArguments args) 16 | { 17 | var facts = new FactCollection(); 18 | foreach (var provider in _providers) 19 | { 20 | foreach (var (name, value) in provider.GetFacts(args)) 21 | { 22 | facts.Add(name, value ?? string.Empty); 23 | } 24 | } 25 | 26 | return facts; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Cupboard/IStatusUpdater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Spectre.Console; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | internal interface IStatusUpdater 7 | { 8 | void Update(string markup); 9 | } 10 | 11 | internal sealed class StatusUpdater : IStatusUpdater 12 | { 13 | private readonly StatusContext _context; 14 | 15 | public StatusUpdater(StatusContext context) 16 | { 17 | _context = context ?? throw new ArgumentNullException(nameof(context)); 18 | } 19 | 20 | public void Update(string markup) 21 | { 22 | if (!string.IsNullOrWhiteSpace(markup)) 23 | { 24 | _context.Status = markup; 25 | } 26 | } 27 | } 28 | 29 | internal sealed class DummyUpdater : IStatusUpdater 30 | { 31 | public void Update(string markup) 32 | { 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Cupboard/IO/WindowsRegistry.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Win32Registry = Microsoft.Win32.Registry; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] 7 | internal sealed class WindowsRegistry : IWindowsRegistry 8 | { 9 | public IWindowsRegistryKey ClassesRoot => new WindowsRegistryKey(Win32Registry.ClassesRoot); 10 | public IWindowsRegistryKey CurrentConfig => new WindowsRegistryKey(Win32Registry.CurrentConfig); 11 | public IWindowsRegistryKey CurrentUser => new WindowsRegistryKey(Win32Registry.CurrentUser); 12 | public IWindowsRegistryKey LocalMachine => new WindowsRegistryKey(Win32Registry.LocalMachine); 13 | public IWindowsRegistryKey PerformanceData => new WindowsRegistryKey(Win32Registry.PerformanceData); 14 | public IWindowsRegistryKey Users => new WindowsRegistryKey(Win32Registry.Users); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Extensions/ResourceExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public static class ResourceExtensions 4 | { 5 | public static IResourceBuilder OnError(this IResourceBuilder builder, ErrorOptions options) 6 | where T : Resource 7 | { 8 | builder.Configure(res => res.Error = options); 9 | return builder; 10 | } 11 | 12 | public static IResourceBuilder OnReboot(this IResourceBuilder builder, RebootOptions options) 13 | where T : Resource 14 | { 15 | builder.Configure(res => res.Reboot = options); 16 | return builder; 17 | } 18 | 19 | public static IResourceBuilder RequireAdministrator(this IResourceBuilder builder) 20 | where T : Resource 21 | { 22 | builder.Configure(res => res.RequireAdministrator = true); 23 | return builder; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Cupboard/ExecutionPlan.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | internal sealed class ExecutionPlan : IEnumerable 7 | { 8 | private readonly IReadOnlyList _resources; 9 | 10 | public bool RequiresAdministrator { get; } 11 | public int Count => _resources.Count; 12 | 13 | public ExecutionPlan( 14 | IEnumerable resources, 15 | bool requiresAdministrator) 16 | { 17 | _resources = resources.ToReadOnlyList(); 18 | RequiresAdministrator = requiresAdministrator; 19 | } 20 | 21 | public IEnumerator GetEnumerator() 22 | { 23 | return _resources.GetEnumerator(); 24 | } 25 | 26 | IEnumerator IEnumerable.GetEnumerator() 27 | { 28 | return GetEnumerator(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Sandbox/Facts/RustFactProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Cupboard; 4 | using Spectre.Console.Cli; 5 | using Spectre.IO; 6 | 7 | namespace Sandbox 8 | { 9 | public sealed class RustFactProvider : IFactProvider 10 | { 11 | private readonly ICupboardFileSystem _fileSystem; 12 | private readonly ICupboardEnvironment _environment; 13 | 14 | public RustFactProvider(ICupboardFileSystem fileSystem, ICupboardEnvironment environment) 15 | { 16 | _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); 17 | _environment = environment ?? throw new ArgumentNullException(nameof(environment)); 18 | } 19 | 20 | IEnumerable<(string Name, object Value)> IFactProvider.GetFacts(IRemainingArguments args) 21 | { 22 | var path = new DirectoryPath("~/.cargo").MakeAbsolute(_environment); 23 | yield return ("rust.installed", _fileSystem.Exist(path)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Cupboard.Core/LogLevel.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public enum LogLevel 4 | { 5 | /// 6 | /// Severe errors that cause premature termination. 7 | /// 8 | Fatal, 9 | 10 | /// 11 | /// Other runtime errors or unexpected conditions. 12 | /// 13 | Error, 14 | 15 | /// 16 | /// Use of deprecated APIs, poor use of API, 'almost' errors, other runtime 17 | /// situations that are undesirable or unexpected, but not necessarily "wrong". 18 | /// 19 | Warning, 20 | 21 | /// 22 | /// Interesting runtime events. 23 | /// 24 | Information, 25 | 26 | /// 27 | /// Detailed information on the flow through the system. 28 | /// 29 | Verbose, 30 | 31 | /// 32 | /// Most detailed information. 33 | /// 34 | Debug, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/ResourcesModule.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Cupboard 4 | { 5 | public sealed class ResourcesModule : ServiceModule 6 | { 7 | public override void Configure(IServiceCollection services) 8 | { 9 | // Resources 10 | services.AddSingleton(); 11 | services.AddSingleton(); 12 | services.AddSingleton(); 13 | services.AddSingleton(); 14 | services.AddSingleton(); 15 | services.AddSingleton(); 16 | 17 | // Facts 18 | services.AddSingleton(); 19 | services.AddSingleton(); 20 | services.AddSingleton(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/WindowsModule.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Cupboard 4 | { 5 | public sealed class WindowsModule : ServiceModule 6 | { 7 | public override void Configure(IServiceCollection services) 8 | { 9 | // Resources 10 | services.AddSingleton(); 11 | services.AddSingleton(); 12 | services.AddSingleton(); 13 | services.AddSingleton(); 14 | 15 | #pragma warning disable CS0618 // Type or member is obsolete 16 | services.AddSingleton(); 17 | #pragma warning restore CS0618 // Type or member is obsolete 18 | 19 | // Facts 20 | services.AddSingleton(); 21 | services.AddSingleton(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Winget/WingetPackageExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public static class WingetPackageExtensions 4 | { 5 | public static IResourceBuilder Ensure(this IResourceBuilder builder, PackageState state) 6 | { 7 | return builder.Configure(pkg => pkg.Ensure = state); 8 | } 9 | 10 | public static IResourceBuilder Package(this IResourceBuilder builder, string package) 11 | { 12 | return builder.Configure(pkg => pkg.Package = package); 13 | } 14 | 15 | public static IResourceBuilder Force(this IResourceBuilder builder, bool force) 16 | { 17 | return builder.Configure(pkg => pkg.Force = force); 18 | } 19 | 20 | public static IResourceBuilder PackageVersion(this IResourceBuilder builder, string version) 21 | { 22 | return builder.Configure(pkg => pkg.PackageVersion = version); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Cupboard/Cupboard.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 9.0 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Cupboard/ResourceProviderRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | internal sealed class ResourceProviderRepository 7 | { 8 | private readonly Dictionary _lookup; 9 | 10 | public ResourceProviderRepository(IEnumerable providers) 11 | { 12 | _lookup = new Dictionary(); 13 | foreach (var provider in providers) 14 | { 15 | if (_lookup.ContainsKey(provider.ResourceType)) 16 | { 17 | throw new InvalidOperationException( 18 | $"Encountered duplicate providers for {provider.ResourceType}"); 19 | } 20 | 21 | _lookup.Add(provider.ResourceType, provider); 22 | } 23 | } 24 | 25 | public IResourceProvider? GetProvider(Type type) 26 | { 27 | _lookup.TryGetValue(type, out var provider); 28 | return provider; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeWindowsRegistry.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard.Testing 2 | { 3 | public sealed class FakeWindowsRegistry : IWindowsRegistry 4 | { 5 | public IWindowsRegistryKey ClassesRoot { get; } 6 | public IWindowsRegistryKey CurrentConfig { get; } 7 | public IWindowsRegistryKey CurrentUser { get; } 8 | public IWindowsRegistryKey LocalMachine { get; } 9 | public IWindowsRegistryKey PerformanceData { get; } 10 | public IWindowsRegistryKey Users { get; } 11 | 12 | public FakeWindowsRegistry() 13 | { 14 | ClassesRoot = new FakeWindowsRegistryKey(new RegistryPath("HKCR")); 15 | CurrentConfig = new FakeWindowsRegistryKey(new RegistryPath("HKCC")); 16 | CurrentUser = new FakeWindowsRegistryKey(new RegistryPath("HKCU")); 17 | LocalMachine = new FakeWindowsRegistryKey(new RegistryPath("HKLM")); 18 | PerformanceData = new FakeWindowsRegistryKey(new RegistryPath("HKPD")); 19 | Users = new FakeWindowsRegistryKey(new RegistryPath("HKU")); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Cupboard/Cli/Infrastructure/TypeRegistrar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Spectre.Console.Cli; 4 | 5 | namespace Cupboard.Internal 6 | { 7 | internal sealed class TypeRegistrar : ITypeRegistrar 8 | { 9 | private readonly IServiceCollection _provider; 10 | 11 | public TypeRegistrar(IServiceCollection provider) 12 | { 13 | _provider = provider; 14 | } 15 | 16 | public ITypeResolver Build() 17 | { 18 | return new TypeResolver(_provider.BuildServiceProvider()); 19 | } 20 | 21 | public void Register(Type service, Type implementation) 22 | { 23 | _provider.AddSingleton(service, implementation); 24 | } 25 | 26 | public void RegisterInstance(Type service, object implementation) 27 | { 28 | _provider.AddSingleton(service, implementation); 29 | } 30 | 31 | public void RegisterLazy(Type service, Func factory) 32 | { 33 | _provider.AddSingleton(service, _ => factory()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Patrik Svensson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/PowerShell/PowerShellScriptExtensions.cs: -------------------------------------------------------------------------------- 1 | using Spectre.IO; 2 | 3 | namespace Cupboard 4 | { 5 | public static class PowerShellScriptExtensions 6 | { 7 | public static IResourceBuilder Script(this IResourceBuilder builder, FilePath file) 8 | { 9 | builder.Configure(res => res.Script = file); 10 | return builder; 11 | } 12 | 13 | public static IResourceBuilder Command(this IResourceBuilder builder, string command) 14 | { 15 | builder.Configure(res => res.Command = command); 16 | return builder; 17 | } 18 | 19 | public static IResourceBuilder Flavor(this IResourceBuilder builder, PowerShellFlavor flavor) 20 | { 21 | builder.Configure(res => res.Flavor = flavor); 22 | return builder; 23 | } 24 | 25 | public static IResourceBuilder Unless(this IResourceBuilder builder, string script) 26 | { 27 | builder.Configure(res => res.Unless = script); 28 | return builder; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: pull_request 3 | 4 | env: 5 | # Set the DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable to stop wasting time caching packages 6 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 7 | # Disable sending usage data to Microsoft 8 | DOTNET_CLI_TELEMETRY_OPTOUT: true 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | if: "!contains(github.event.head_commit.message, 'skip-ci')" 14 | strategy: 15 | matrix: 16 | kind: ['linux', 'windows', 'macOS'] 17 | include: 18 | - kind: linux 19 | os: ubuntu-latest 20 | - kind: windows 21 | os: windows-latest 22 | - kind: macOS 23 | os: macos-latest 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v2 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: 'Get Git tags' 32 | run: git fetch --tags 33 | shell: bash 34 | 35 | - name: Setup dotnet 36 | uses: actions/setup-dotnet@v1 37 | with: 38 | dotnet-version: 5.0.301 39 | 40 | - name: Build 41 | shell: bash 42 | run: | 43 | dotnet tool restore 44 | dotnet cake -------------------------------------------------------------------------------- /src/Cupboard.Tests/Cupboard.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Extensions/IWindowsRegistryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | public static class IWindowsRegistryExtensions 6 | { 7 | public static IWindowsRegistryKey? GetKey(this IWindowsRegistry registry, RegistryPath path, bool writable) 8 | { 9 | if (registry is null) 10 | { 11 | throw new ArgumentNullException(nameof(registry)); 12 | } 13 | 14 | if (path is null) 15 | { 16 | throw new ArgumentNullException(nameof(path)); 17 | } 18 | 19 | var root = path.Hive switch 20 | { 21 | RegistryHive.ClassesRoot => registry.ClassesRoot, 22 | RegistryHive.CurrentUser => registry.CurrentUser, 23 | RegistryHive.LocalMachine => registry.LocalMachine, 24 | RegistryHive.Users => registry.Users, 25 | RegistryHive.CurrentConfig => registry.CurrentConfig, 26 | RegistryHive.PerformanceData => registry.PerformanceData, 27 | _ => throw new InvalidOperationException("Unknown registry root"), 28 | }; 29 | 30 | return root.OpenSubKey(path.SubKey, writable); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Cupboard/Extensions/RegistryValueKindExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Cupboard.Internal 4 | { 5 | internal static class RegistryValueKindExtensions 6 | { 7 | [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] 8 | public static Microsoft.Win32.RegistryValueKind ToWin32(this RegistryValueKind type) 9 | { 10 | return type switch 11 | { 12 | RegistryValueKind.None => Microsoft.Win32.RegistryValueKind.None, 13 | RegistryValueKind.Unknown => Microsoft.Win32.RegistryValueKind.Unknown, 14 | RegistryValueKind.String => Microsoft.Win32.RegistryValueKind.String, 15 | RegistryValueKind.ExpandString => Microsoft.Win32.RegistryValueKind.ExpandString, 16 | RegistryValueKind.Binary => Microsoft.Win32.RegistryValueKind.Binary, 17 | RegistryValueKind.DWord => Microsoft.Win32.RegistryValueKind.DWord, 18 | RegistryValueKind.MultiString => Microsoft.Win32.RegistryValueKind.MultiString, 19 | RegistryValueKind.QWord => Microsoft.Win32.RegistryValueKind.QWord, 20 | _ => Microsoft.Win32.RegistryValueKind.Unknown, 21 | }; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Cupboard.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | [*.cs] 3 | 4 | # Default severity for analyzer diagnostics with category 'StyleCop.CSharp.DocumentationRules' 5 | dotnet_analyzer_diagnostic.category-StyleCop.CSharp.DocumentationRules.severity = none 6 | 7 | # CA1707: Identifiers should not contain underscores 8 | dotnet_diagnostic.CA1707.severity = none 9 | 10 | # SA1600: Elements should be documented 11 | dotnet_diagnostic.SA1600.severity = none 12 | 13 | # SA1601: Partial elements should be documented 14 | dotnet_diagnostic.SA1601.severity = none 15 | 16 | # SA1200: Using directives should be placed correctly 17 | dotnet_diagnostic.SA1200.severity = none 18 | 19 | # CS1591: Missing XML comment for publicly visible type or member 20 | dotnet_diagnostic.CS1591.severity = none 21 | 22 | # SA1210: Using directives should be ordered alphabetically by namespace 23 | dotnet_diagnostic.SA1210.severity = none 24 | 25 | # CA1034: Nested types should not be visible 26 | dotnet_diagnostic.CA1034.severity = none 27 | 28 | # CA2000: Dispose objects before losing scope 29 | dotnet_diagnostic.CA2000.severity = none 30 | 31 | # SA1118: Parameter should not span multiple lines 32 | dotnet_diagnostic.SA1118.severity = none 33 | 34 | # CA1031: Do not catch general exception types 35 | dotnet_diagnostic.CA1031.severity = none -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Registry/Obsolete/RegistryKeyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard 4 | { 5 | [Obsolete("Please use the RegistryValue resource instead")] 6 | public static class RegistryKeyExtensions 7 | { 8 | public static IResourceBuilder Ensure(this IResourceBuilder builder, RegistryKeyState state) 9 | { 10 | return builder.Configure(reg => reg.State = state); 11 | } 12 | 13 | public static IResourceBuilder Path(this IResourceBuilder builder, string path) 14 | { 15 | return builder.Configure(reg => reg.Path = new RegistryKeyPath(path)); 16 | } 17 | 18 | public static IResourceBuilder Value(this IResourceBuilder builder, object value) 19 | { 20 | return builder.Configure(reg => reg.Value = value); 21 | } 22 | 23 | public static IResourceBuilder Value(this IResourceBuilder builder, object value, RegistryKeyValueKind kind) 24 | { 25 | return builder.Configure(reg => 26 | { 27 | reg.State = RegistryKeyState.Exist; 28 | reg.Value = value; 29 | reg.ValueKind = kind; 30 | }); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Cupboard/IO/CupboardEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Spectre.IO; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | internal sealed class CupboardEnvironment : ICupboardEnvironment 7 | { 8 | private readonly IEnvironment _environment; 9 | 10 | public DirectoryPath WorkingDirectory => _environment.WorkingDirectory; 11 | public DirectoryPath HomeDirectory => _environment.HomeDirectory; 12 | public IPlatform Platform => _environment.Platform; 13 | 14 | public CupboardEnvironment(IPlatform platform) 15 | { 16 | _environment = new Spectre.IO.Environment(platform); 17 | } 18 | 19 | public string? GetEnvironmentVariable(string variable) 20 | { 21 | return _environment.GetEnvironmentVariable(variable); 22 | } 23 | 24 | public IDictionary GetEnvironmentVariables() 25 | { 26 | return _environment.GetEnvironmentVariables(); 27 | } 28 | 29 | public void SetWorkingDirectory(DirectoryPath path) 30 | { 31 | _environment.SetWorkingDirectory(path); 32 | } 33 | 34 | public FilePath GetTempFilePath() 35 | { 36 | return new FilePath(System.IO.Path.GetTempFileName()).MakeAbsolute(this); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Chocolatey/ChocolateyPackageExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public static class ChocolateyPackageExtensions 4 | { 5 | public static IResourceBuilder Ensure(this IResourceBuilder builder, PackageState state) 6 | { 7 | return builder.Configure(pkg => pkg.Ensure = state); 8 | } 9 | 10 | public static IResourceBuilder Package(this IResourceBuilder builder, string package) 11 | { 12 | return builder.Configure(pkg => pkg.Package = package); 13 | } 14 | 15 | public static IResourceBuilder IncludePreRelease(this IResourceBuilder builder) 16 | { 17 | return builder.Configure(pkg => pkg.PreRelease = true); 18 | } 19 | 20 | public static IResourceBuilder IgnoreChecksum(this IResourceBuilder builder) 21 | { 22 | return builder.Configure(pkg => pkg.IgnoreChecksum = true); 23 | } 24 | 25 | public static IResourceBuilder PackageParameters(this IResourceBuilder builder, string packageParameters) 26 | { 27 | return builder.Configure(pkg => pkg.PackageParameters = packageParameters); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Sandbox/Manifests/Chocolatey.cs: -------------------------------------------------------------------------------- 1 | using Cupboard; 2 | 3 | namespace Sandbox 4 | { 5 | public sealed class Chocolatey : Manifest 6 | { 7 | public override void Execute(ManifestContext context) 8 | { 9 | // Do not run this in sandbox 10 | if (context.Facts["windows"]["sandbox"]) 11 | { 12 | return; 13 | } 14 | 15 | // Download 16 | context.Resource("https://chocolatey.org/install.ps1") 17 | .ToFile("~/install-chocolatey.ps1"); 18 | 19 | // Set execution policy 20 | context.Resource("Set execution policy") 21 | .Path(@"HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell") 22 | .Value("ExecutionPolicy") 23 | .Data("Unrestricted", RegistryValueKind.String); 24 | 25 | // Install 26 | context.Resource("Install Chocolatey") 27 | .Script("~/install-chocolatey.ps1") 28 | .Flavor(PowerShellFlavor.PowerShell) 29 | .RequireAdministrator() 30 | .Unless("if (Test-Path \"$($env:ProgramData)/chocolatey/choco.exe\") { exit 1 }") 31 | .After("Set execution policy") 32 | .After("https://chocolatey.org/install.ps1"); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Cupboard/Extensions/RegistryKeyValueKindExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | [Obsolete("Please use RegistryValueKindExtensions instead")] 7 | internal static class RegistryKeyValueKindExtensions 8 | { 9 | [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] 10 | public static Microsoft.Win32.RegistryValueKind ToWin32(this RegistryKeyValueKind type) 11 | { 12 | return type switch 13 | { 14 | RegistryKeyValueKind.None => Microsoft.Win32.RegistryValueKind.None, 15 | RegistryKeyValueKind.Unknown => Microsoft.Win32.RegistryValueKind.Unknown, 16 | RegistryKeyValueKind.String => Microsoft.Win32.RegistryValueKind.String, 17 | RegistryKeyValueKind.ExpandString => Microsoft.Win32.RegistryValueKind.ExpandString, 18 | RegistryKeyValueKind.Binary => Microsoft.Win32.RegistryValueKind.Binary, 19 | RegistryKeyValueKind.DWord => Microsoft.Win32.RegistryValueKind.DWord, 20 | RegistryKeyValueKind.MultiString => Microsoft.Win32.RegistryValueKind.MultiString, 21 | RegistryKeyValueKind.QWord => Microsoft.Win32.RegistryValueKind.QWord, 22 | _ => Microsoft.Win32.RegistryValueKind.Unknown, 23 | }; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Cupboard/ResourceComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | internal sealed class ResourceComparer : IEqualityComparer 7 | { 8 | private readonly IEqualityComparer _typeComparer; 9 | private readonly IEqualityComparer _stringComparer; 10 | 11 | public ResourceComparer() 12 | { 13 | _typeComparer = EqualityComparer.Default; 14 | _stringComparer = StringComparer.OrdinalIgnoreCase; 15 | } 16 | 17 | public bool Equals(IResourceIdentity? x, IResourceIdentity? y) 18 | { 19 | if (x == null && y == null) 20 | { 21 | return true; 22 | } 23 | 24 | if (x == null || y == null) 25 | { 26 | return false; 27 | } 28 | 29 | return _typeComparer.Equals(x.ResourceType, y.ResourceType) 30 | && _stringComparer.Equals(x.Name, y.Name); 31 | } 32 | 33 | public int GetHashCode(IResourceIdentity obj) 34 | { 35 | unchecked 36 | { 37 | var hash = 27; 38 | hash = (13 * hash) + _typeComparer.GetHashCode(obj.ResourceType); 39 | hash = (13 * hash) + _stringComparer.GetHashCode(obj.Name); 40 | return hash; 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Sandbox/Manifests/VSCode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Cupboard; 3 | 4 | namespace Sandbox 5 | { 6 | public sealed class VSCode : Manifest 7 | { 8 | public override void Execute(ManifestContext context) 9 | { 10 | if (!context.Facts.IsX86OrX64()) 11 | { 12 | return; 13 | } 14 | 15 | var packages = new List 16 | { 17 | "cake-build.cake-vscode", 18 | "matklad.rust-analyzer", 19 | "ms-vscode.powershell", 20 | "bungcip.better-toml", 21 | "ms-azuretools.vscode-docker", 22 | "octref.vetur", 23 | "ms-vscode-remote.remote-wsl", 24 | "jolaleye.horizon-theme-vscode", 25 | "vscode-icons-team.vscode-icons", 26 | "hediet.vscode-drawio", 27 | }; 28 | 29 | // VSCode 30 | context.Resource("vscode") 31 | .Ensure(PackageState.Installed) 32 | .After("Install Chocolatey"); 33 | 34 | // Extensions 35 | foreach (var package in packages) 36 | { 37 | context.Resource(package) 38 | .Ensure(PackageState.Installed) 39 | .After("vscode"); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Cupboard/ColorPalette.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Spectre.Console; 4 | 5 | namespace Cupboard.Internal 6 | { 7 | internal sealed class ColorPalette 8 | where T : notnull 9 | { 10 | private readonly Dictionary _colors; 11 | 12 | public ColorPalette(IEnumerable items, IEqualityComparer? comparer = null) 13 | { 14 | _colors = new Dictionary(comparer ?? EqualityComparer.Default); 15 | 16 | var random = new Random(DateTime.Now.Millisecond); 17 | var colors = new HashSet(); 18 | foreach (var item in items) 19 | { 20 | Color color; 21 | while (true) 22 | { 23 | color = new Color( 24 | (byte)random.Next(70, 200), 25 | (byte)random.Next(100, 225), 26 | (byte)random.Next(100, 230)); 27 | 28 | if (colors.Add(color)) 29 | { 30 | break; 31 | } 32 | } 33 | 34 | _colors[item] = color; 35 | } 36 | } 37 | 38 | public Color GetColor(T item) 39 | { 40 | if (_colors.TryGetValue(item, out var color)) 41 | { 42 | return color; 43 | } 44 | 45 | return Color.White; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Misc folders 2 | [Bb]in/ 3 | [Oo]bj/ 4 | [Tt]emp/ 5 | [Pp]ackages/ 6 | /.artifacts/ 7 | /[Tt]ools/ 8 | 9 | # Cakeup 10 | cakeup-x86_64-latest.exe 11 | 12 | # .NET Core CLI 13 | /.dotnet/ 14 | /.packages/ 15 | dotnet-install.sh* 16 | *.lock.json 17 | 18 | # Visual Studio 19 | .vs/ 20 | .vscode/ 21 | launchSettings.json 22 | *.sln.ide/ 23 | 24 | # Rider 25 | src/.idea/**/workspace.xml 26 | src/.idea/**/tasks.xml 27 | src/.idea/dictionaries 28 | src/.idea/**/dataSources/ 29 | src/.idea/**/dataSources.ids 30 | src/.idea/**/dataSources.xml 31 | src/.idea/**/dataSources.local.xml 32 | src/.idea/**/sqlDataSources.xml 33 | src/.idea/**/dynamic.xml 34 | src/.idea/**/uiDesigner.xml 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | *.userprefs 44 | *.GhostDoc.xml 45 | *StyleCop.Cache 46 | 47 | # Build results 48 | [Dd]ebug/ 49 | [Rr]elease/ 50 | x64/ 51 | *_i.c 52 | *_p.c 53 | *.ilk 54 | *.meta 55 | *.obj 56 | *.pch 57 | *.pdb 58 | *.pgc 59 | *.pgd 60 | *.rsp 61 | *.sbr 62 | *.tlb 63 | *.tli 64 | *.tlh 65 | *.tmp 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | 71 | # Visual Studio profiler 72 | *.psess 73 | *.vsp 74 | *.vspx 75 | 76 | # ReSharper is a .NET coding add-in 77 | _ReSharper* 78 | 79 | # NCrunch 80 | *.ncrunch* 81 | .*crunch*.local.xml 82 | _NCrunch_* 83 | 84 | # NuGet Packages Directory 85 | packages 86 | 87 | # Windows 88 | Thumbs.db 89 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/File/FileExtensions.cs: -------------------------------------------------------------------------------- 1 | using Spectre.IO; 2 | 3 | namespace Cupboard 4 | { 5 | public static class FileExtensions 6 | { 7 | public static IResourceBuilder Source(this IResourceBuilder builder, FilePath path) 8 | { 9 | return builder.Configure(file => file.Source = path); 10 | } 11 | 12 | public static IResourceBuilder Destination(this IResourceBuilder builder, FilePath path) 13 | { 14 | return builder.Configure(file => file.Destination = path); 15 | } 16 | 17 | public static IResourceBuilder SymbolicLink(this IResourceBuilder builder) 18 | { 19 | return builder.Configure(file => file.SymbolicLink = true); 20 | } 21 | 22 | public static IResourceBuilder Ensure(this IResourceBuilder builder, FileState state) 23 | { 24 | return builder.Configure(file => file.Ensure = state); 25 | } 26 | 27 | public static IResourceBuilder Permissions(this IResourceBuilder builder, string chmod) 28 | { 29 | var permissions = ChmodParser.Parse(chmod); 30 | return builder.Configure(file => file.Permissions = permissions); 31 | } 32 | 33 | public static IResourceBuilder Permissions(this IResourceBuilder builder, Chmod permissions) 34 | { 35 | return builder.Configure(file => file.Permissions = permissions); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Cupboard.Tests/FactCollectionTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using Xunit; 3 | 4 | namespace Cupboard.Tests 5 | { 6 | public sealed class FactCollectionTests 7 | { 8 | [Theory] 9 | [InlineData("foo")] 10 | [InlineData("FOO")] 11 | [InlineData("Foo")] 12 | [InlineData("FoO")] 13 | public void Should_Consider_Fact_Name_Case_Insensitive(string name) 14 | { 15 | // Then 16 | var c = new FactCollection 17 | { 18 | { "foo", 1 }, 19 | }; 20 | 21 | // When 22 | int value = c[name]; 23 | 24 | // Then 25 | value.ShouldBe(1); 26 | } 27 | 28 | [Fact] 29 | public void Should_Get_Value_By_Path_Segments() 30 | { 31 | // Then 32 | var c = new FactCollection 33 | { 34 | { "foo.bar.baz", 1 }, 35 | }; 36 | 37 | // When 38 | int value = c["foo"]["bar"]["baz"]; 39 | 40 | // Then 41 | value.ShouldBe(1); 42 | } 43 | 44 | [Fact] 45 | public void Should_Get_Value_By_Full_Path() 46 | { 47 | // Then 48 | var c = new FactCollection 49 | { 50 | { "foo.bar.baz", 1 }, 51 | }; 52 | 53 | // When 54 | int value = c["foo.bar.baz"]; 55 | 56 | // Then 57 | value.ShouldBe(1); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Report.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Cupboard 7 | { 8 | public sealed class Report : IEnumerable 9 | { 10 | public IReadOnlyList Items { get; } 11 | public FactCollection Facts { get; } 12 | public bool RequiresAdministrator { get; } 13 | public bool DryRun { get; } 14 | public bool PendingReboot { get; } 15 | 16 | public int Count => Items.Count; 17 | public bool Successful { get; } 18 | 19 | public Report(IEnumerable items, FactCollection facts, bool requiresAdministrator, bool dryRun, bool pendingReboot) 20 | { 21 | Items = items.ToReadOnlyList(); 22 | Facts = facts ?? throw new ArgumentNullException(nameof(facts)); 23 | RequiresAdministrator = requiresAdministrator; 24 | DryRun = dryRun; 25 | PendingReboot = pendingReboot; 26 | 27 | // Exclude Unknown states if this is a dry run 28 | var temp = DryRun ? Items.Where(x => x.State != ResourceState.Unknown) : Items; 29 | Successful = temp.All(x => !x.State.IsError()); 30 | } 31 | 32 | public IEnumerator GetEnumerator() 33 | { 34 | return Items.GetEnumerator(); 35 | } 36 | 37 | IEnumerator IEnumerable.GetEnumerator() 38 | { 39 | return GetEnumerator(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Cupboard.Core/ResourceProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Cupboard 5 | { 6 | public abstract class ResourceProvider : AsyncResourceProvider 7 | where TResource : Resource 8 | { 9 | public abstract ResourceState Run(IExecutionContext context, TResource resource); 10 | 11 | public sealed override Task RunAsync(IExecutionContext context, TResource resource) 12 | { 13 | return Task.FromResult(Run(context, resource)); 14 | } 15 | } 16 | 17 | public abstract class AsyncResourceProvider : IResourceProvider 18 | where TResource : Resource 19 | { 20 | public Type ResourceType => typeof(TResource); 21 | 22 | public virtual bool RequireAdministrator(FactCollection facts) 23 | { 24 | return false; 25 | } 26 | 27 | public virtual bool CanRun(FactCollection facts) 28 | { 29 | return true; 30 | } 31 | 32 | public abstract TResource Create(string name); 33 | 34 | public abstract Task RunAsync(IExecutionContext context, TResource resource); 35 | 36 | Task IResourceProvider.RunAsync(IExecutionContext context, Resource resource) 37 | { 38 | return RunAsync(context, (TResource)resource); 39 | } 40 | 41 | Resource IResourceProvider.Create(string name) 42 | { 43 | return Create(name); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Cupboard/CupboardHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Spectre.Console; 5 | using Spectre.Console.Cli; 6 | 7 | namespace Cupboard 8 | { 9 | public sealed class CupboardHost 10 | { 11 | private readonly IAnsiConsole _console; 12 | private readonly IHost _host; 13 | private readonly bool _propagateExceptions; 14 | 15 | internal CupboardHost(IAnsiConsole console, IHost host, bool propagateExceptions) 16 | { 17 | _console = console ?? throw new ArgumentNullException(nameof(console)); 18 | _host = host ?? throw new ArgumentNullException(nameof(host)); 19 | _propagateExceptions = propagateExceptions; 20 | } 21 | 22 | public static CupboardHostBuilder CreateBuilder() 23 | { 24 | return new CupboardHostBuilder(); 25 | } 26 | 27 | public int Run(string[] args) 28 | { 29 | try 30 | { 31 | var app = _host.Services.GetRequiredService(); 32 | return app.Run(args); 33 | } 34 | catch (Exception ex) when (!_propagateExceptions) 35 | { 36 | _console.WriteLine(); 37 | _console.Write(new Panel("An error occured during execution").BorderColor(Color.Red).RoundedBorder()); 38 | _console.WriteException(ex, ExceptionFormats.ShortenEverything); 39 | 40 | return -1; 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeCupboardFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Spectre.IO; 3 | using Spectre.IO.Testing; 4 | 5 | namespace Cupboard.Testing 6 | { 7 | public sealed class FakeCupboardFileSystem : ICupboardFileSystem 8 | { 9 | private readonly FakeFileSystem _fileSystem; 10 | 11 | public IFileProvider File => _fileSystem.File; 12 | public IDirectoryProvider Directory => _fileSystem.Directory; 13 | 14 | public FakeCupboardFileSystem(ICupboardEnvironment environment) 15 | { 16 | _fileSystem = new FakeFileSystem(environment); 17 | } 18 | 19 | public FakeDirectory CreateDirectory(DirectoryPath path) 20 | { 21 | return _fileSystem.CreateDirectory(path); 22 | } 23 | 24 | public FakeFile CreateFile(FilePath path, FileAttributes attributes = 0) 25 | { 26 | return _fileSystem.CreateFile(path, attributes); 27 | } 28 | 29 | public FakeFile CreateFile(FilePath path, byte[] contentsBytes) 30 | { 31 | return _fileSystem.CreateFile(path, contentsBytes); 32 | } 33 | 34 | public void EnsureFileDoesNotExist(FilePath path) 35 | { 36 | _fileSystem.EnsureFileDoesNotExist(path); 37 | } 38 | 39 | public FakeDirectory GetFakeDirectory(DirectoryPath path) 40 | { 41 | return _fileSystem.GetFakeDirectory(path); 42 | } 43 | 44 | public FakeFile GetFakeFile(FilePath path) 45 | { 46 | return _fileSystem.GetFakeFile(path); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Sandbox/Manifests/Rust.cs: -------------------------------------------------------------------------------- 1 | using Cupboard; 2 | 3 | namespace Sandbox 4 | { 5 | public sealed class Rust : Manifest 6 | { 7 | public override void Execute(ManifestContext context) 8 | { 9 | // Download directory 10 | context.Resource("~/Downloads") 11 | .Ensure(DirectoryState.Present); 12 | 13 | if (context.Facts["rust.installed"]) 14 | { 15 | return; 16 | } 17 | 18 | if (context.Facts.IsWindows()) 19 | { 20 | // Download Rust installer 21 | context.Resource("https://win.rustup.rs/x86_64") 22 | .ToFile("~/Downloads/rustup-init.exe"); 23 | 24 | // Run Rust installer 25 | context.Resource("~/Downloads/rustup-init.exe") 26 | .Arguments("-y") 27 | .ValidExitCodes(0, 1) 28 | .After("https://win.rustup.rs/x86_64"); 29 | } 30 | else 31 | { 32 | // Download Rust installer script 33 | context.Resource("https://sh.rustup.rs") 34 | .Permissions("700") 35 | .After("~/Downloads") 36 | .ToFile("~/Downloads/rustup.sh"); 37 | 38 | // Run Rust installer 39 | context.Resource("~/Downloads/rustup.sh") 40 | .Arguments("-y") 41 | .ValidExitCodes(0, 1) 42 | .After("https://sh.rustup.rs"); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Registry/RegistryValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public static class RegistryValueExtensions 4 | { 5 | public static IResourceBuilder Ensure(this IResourceBuilder builder, RegistryKeyState state) 6 | { 7 | return builder.Configure(reg => reg.State = state); 8 | } 9 | 10 | public static IResourceBuilder Path(this IResourceBuilder builder, string path) 11 | { 12 | return builder.Configure(reg => reg.Path = new RegistryPath(path)); 13 | } 14 | 15 | public static IResourceBuilder Value(this IResourceBuilder builder, string value) 16 | { 17 | return builder.Configure(reg => reg.Value = value); 18 | } 19 | 20 | public static IResourceBuilder Data(this IResourceBuilder builder, string data) 21 | { 22 | return builder.Configure(reg => reg.Data = data); 23 | } 24 | 25 | public static IResourceBuilder Data(this IResourceBuilder builder, object data) 26 | { 27 | return builder.Configure(reg => reg.Data = data); 28 | } 29 | 30 | public static IResourceBuilder Data(this IResourceBuilder builder, object data, RegistryValueKind kind) 31 | { 32 | return builder.Configure(reg => 33 | { 34 | reg.State = RegistryKeyState.Exist; 35 | reg.Data = data; 36 | reg.ValueKind = kind; 37 | }); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/Download/DownloadExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Spectre.IO; 3 | 4 | namespace Cupboard 5 | { 6 | public static class DownloadExtensions 7 | { 8 | public static IResourceBuilder FromUrl(this IResourceBuilder builder, string url) 9 | { 10 | builder.Configure(res => res.Url = new Uri(url)); 11 | return builder; 12 | } 13 | 14 | public static IResourceBuilder Permissions(this IResourceBuilder builder, string permissions) 15 | { 16 | builder.Configure(res => res.Permissions = Chmod.Parse(permissions)); 17 | return builder; 18 | } 19 | 20 | public static IResourceBuilder Permissions(this IResourceBuilder builder, Chmod permissions) 21 | { 22 | builder.Configure(res => res.Permissions = permissions); 23 | return builder; 24 | } 25 | 26 | public static IResourceBuilder FromUrl(this IResourceBuilder builder, Uri url) 27 | { 28 | builder.Configure(res => res.Url = url); 29 | return builder; 30 | } 31 | 32 | public static IResourceBuilder ToFile(this IResourceBuilder builder, FilePath path) 33 | { 34 | builder.Configure(res => res.Destination = path); 35 | return builder; 36 | } 37 | 38 | public static IResourceBuilder ToDirectory(this IResourceBuilder builder, DirectoryPath path) 39 | { 40 | builder.Configure(res => res.Destination = path); 41 | return builder; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeWindowsRegistryKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard.Testing 4 | { 5 | public sealed class FakeWindowsRegistryKey : IWindowsRegistryKey 6 | { 7 | public RegistryPath Path { get; } 8 | 9 | public FakeWindowsRegistryKey(RegistryPath path) 10 | { 11 | Path = path ?? throw new ArgumentNullException(nameof(path)); 12 | } 13 | 14 | public IWindowsRegistryKey? CreateSubKey(string name, bool writable) 15 | { 16 | throw new NotSupportedException(); 17 | } 18 | 19 | public int GetValueCount() 20 | { 21 | throw new NotSupportedException(); 22 | } 23 | 24 | public void DeleteValue(string name) 25 | { 26 | throw new NotSupportedException(); 27 | } 28 | 29 | public object? GetValue(string name) 30 | { 31 | throw new NotSupportedException(); 32 | } 33 | 34 | public bool ValueExists(string name) 35 | { 36 | throw new NotSupportedException(); 37 | } 38 | 39 | public IWindowsRegistryKey? OpenSubKey(string name, bool writable) 40 | { 41 | throw new NotSupportedException(); 42 | } 43 | 44 | [Obsolete("Please use SetValue overload accepting a RegistryValueKind instead")] 45 | public void SetValue(string name, object value, RegistryKeyValueKind registryValueKind) 46 | { 47 | throw new NotSupportedException(); 48 | } 49 | 50 | public void SetValue(string name, object value, RegistryValueKind kind) 51 | { 52 | throw new NotSupportedException(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Cupboard.Core/ResourceBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard 5 | { 6 | public abstract class ResourceBuilder 7 | { 8 | public string Name { get; } 9 | 10 | public Type Type { get; set; } 11 | 12 | public List> RunBefore { get; } 13 | 14 | public List> RunAfter { get; } 15 | 16 | public List> Configurations { get; } 17 | 18 | protected ResourceBuilder(string name, Type type) 19 | { 20 | Name = name; 21 | Type = type; 22 | RunBefore = new List>(); 23 | RunAfter = new List>(); 24 | Configurations = new List>(); 25 | } 26 | } 27 | 28 | public sealed class ResourceBuilder : ResourceBuilder, IResourceBuilder 29 | where TResource : Resource 30 | { 31 | public ResourceBuilder(string name) 32 | : base(name, typeof(TResource)) 33 | { 34 | } 35 | 36 | public IResourceBuilder Before(string name) 37 | { 38 | RunBefore.Add(Tuple.Create(typeof(TOther), name)); 39 | return this; 40 | } 41 | 42 | public IResourceBuilder After(string name) 43 | { 44 | RunAfter.Add(Tuple.Create(typeof(TOther), name)); 45 | return this; 46 | } 47 | 48 | public IResourceBuilder Configure(Action action) 49 | { 50 | Configurations.Add(resource => action((TResource)resource)); 51 | return this; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeCupboardEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Spectre.IO; 3 | using Spectre.IO.Testing; 4 | 5 | namespace Cupboard.Testing 6 | { 7 | public sealed class FakeCupboardEnvironment : ICupboardEnvironment 8 | { 9 | private readonly FakeEnvironment _environment; 10 | 11 | public DirectoryPath WorkingDirectory => _environment.WorkingDirectory; 12 | public DirectoryPath HomeDirectory => _environment.HomeDirectory; 13 | public IPlatform Platform => _environment.Platform; 14 | 15 | public FakeCupboardEnvironment(PlatformFamily family, bool is64Bit = true) 16 | { 17 | _environment = new FakeEnvironment(family, is64Bit); 18 | } 19 | 20 | public static FakeCupboardEnvironment CreateUnixEnvironment(bool is64Bit = true) 21 | { 22 | return new FakeCupboardEnvironment(PlatformFamily.Linux, is64Bit); 23 | } 24 | 25 | public static FakeCupboardEnvironment CreateWindowsEnvironment(bool is64Bit = true) 26 | { 27 | return new FakeCupboardEnvironment(PlatformFamily.Windows, is64Bit); 28 | } 29 | 30 | public string? GetEnvironmentVariable(string variable) 31 | { 32 | return _environment.GetEnvironmentVariable(variable); 33 | } 34 | 35 | public IDictionary GetEnvironmentVariables() 36 | { 37 | return _environment.GetEnvironmentVariables(); 38 | } 39 | 40 | public FilePath GetTempFilePath() 41 | { 42 | return new FilePath("C:/Temp/fake.ps1").MakeAbsolute(_environment); 43 | } 44 | 45 | public void SetWorkingDirectory(DirectoryPath path) 46 | { 47 | _environment.SetWorkingDirectory(path); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Cupboard/Cli/Infrastructure/VerbosityConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Globalization; 5 | 6 | namespace Cupboard.Internal 7 | { 8 | internal sealed class VerbosityConverter : TypeConverter 9 | { 10 | private readonly Dictionary _lookup; 11 | 12 | public VerbosityConverter() 13 | { 14 | _lookup = new Dictionary(StringComparer.OrdinalIgnoreCase) 15 | { 16 | { "q", Verbosity.Quiet }, 17 | { "quiet", Verbosity.Quiet }, 18 | { "m", Verbosity.Minimal }, 19 | { "minimal", Verbosity.Minimal }, 20 | { "n", Verbosity.Normal }, 21 | { "normal", Verbosity.Normal }, 22 | { "v", Verbosity.Verbose }, 23 | { "verbose", Verbosity.Verbose }, 24 | { "d", Verbosity.Diagnostic }, 25 | { "diagnostic", Verbosity.Diagnostic }, 26 | }; 27 | } 28 | 29 | /// 30 | public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) 31 | { 32 | if (value is string stringValue) 33 | { 34 | var result = _lookup.TryGetValue(stringValue, out var verbosity); 35 | if (!result) 36 | { 37 | throw new InvalidOperationException( 38 | string.Format( 39 | CultureInfo.InvariantCulture, 40 | "The value '{0}' is not a valid verbosity.", 41 | value)); 42 | } 43 | 44 | return verbosity; 45 | } 46 | 47 | throw new NotSupportedException("Can't convert value to verbosity."); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/WmiFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using Microsoft.Management.Infrastructure; 5 | using Spectre.Console.Cli; 6 | 7 | namespace Cupboard 8 | { 9 | internal sealed class WmiFacts : IFactProvider 10 | { 11 | private static readonly Dictionary _properties; 12 | 13 | static WmiFacts() 14 | { 15 | _properties = new Dictionary(StringComparer.OrdinalIgnoreCase) 16 | { 17 | { "Version", "wmi.os.version" }, 18 | { "WindowsDirectory", "wmi.os.windows_dir" }, 19 | { "FreeVirtualMemory", "wmi.os.free_virtual_mem" }, 20 | { "FreePhysicalMemory", "wmi.os.free_physical_mem" }, 21 | { "BuildNumber", "wmi.os.build" }, 22 | { "TotalVirtualMemorySize", "wmi.os.total_virtual_mem" }, 23 | { "TotalVisibleMemorySize", "wmi.os.total_mem" }, 24 | }; 25 | } 26 | 27 | public IEnumerable<(string Name, object Value)> GetFacts(IRemainingArguments args) 28 | { 29 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 30 | { 31 | yield break; 32 | } 33 | 34 | var cimSession = CimSession.Create(null); 35 | var inst = cimSession.GetInstance("root\\cimv2", new CimInstance("Win32_OperatingSystem", "root\\cimv2")); 36 | 37 | foreach (var prop in inst.CimInstanceProperties) 38 | { 39 | if (_properties.ContainsKey(prop.Name)) 40 | { 41 | yield return (_properties[prop.Name], prop.Value); 42 | } 43 | } 44 | 45 | if (inst.CimInstanceProperties.Count > 0) 46 | { 47 | yield return ("wmi.os", true); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Cupboard/Cli/FactCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using Spectre.Console; 5 | using Spectre.Console.Cli; 6 | 7 | namespace Cupboard.Internal 8 | { 9 | internal sealed class FactCommand : Command 10 | { 11 | private readonly IFactBuilder _builder; 12 | private readonly IAnsiConsole _console; 13 | 14 | public sealed class Settings : CommandSettings 15 | { 16 | [CommandOption("--env")] 17 | public bool Environment { get; set; } 18 | } 19 | 20 | public FactCommand(IFactBuilder builder, IAnsiConsole console) 21 | { 22 | _builder = builder ?? throw new ArgumentNullException(nameof(builder)); 23 | _console = console ?? throw new ArgumentNullException(nameof(console)); 24 | } 25 | 26 | public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) 27 | { 28 | var facts = _builder.Build(context.Remaining); 29 | 30 | var table = new Table().BorderColor(Color.Grey); 31 | table.AddColumns("[grey]Path[/]", "[grey]Type[/]", "[grey]Value[/]"); 32 | 33 | foreach (var fact in facts.OrderBy(f => f.FullName)) 34 | { 35 | if (fact.FullName.StartsWith("env.", StringComparison.OrdinalIgnoreCase) && !settings.Environment) 36 | { 37 | continue; 38 | } 39 | 40 | var value = fact.Value?.ToString() ?? string.Empty; 41 | value = value.Replace("\u001b", "ESC"); 42 | 43 | table.AddRow( 44 | fact.FullName, 45 | fact.Value?.GetType().Name ?? "?", 46 | "[grey]" + value.EscapeMarkup() + "[/]"); 47 | } 48 | 49 | _console.Write(table); 50 | 51 | return 0; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/ChmodParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard 5 | { 6 | public static class ChmodParser 7 | { 8 | public static Chmod Parse(string pattern) 9 | { 10 | if (pattern == null) 11 | { 12 | throw new ArgumentNullException(nameof(pattern)); 13 | } 14 | 15 | if (pattern.Length != 3 && pattern.Length != 4) 16 | { 17 | throw new ArgumentException("Invalid pattern.", nameof(pattern)); 18 | } 19 | 20 | var mode = SpecialMode.None; 21 | if (pattern.Length == 4) 22 | { 23 | mode = ParseSpecialMode(pattern[0]); 24 | pattern = pattern.Substring(1, 3); 25 | } 26 | 27 | var queue = new Queue(new[] { ChmodClass.Owner, ChmodClass.Group, ChmodClass.Other }); 28 | var map = new Dictionary 29 | { 30 | { ChmodClass.Owner, Permissions.None }, 31 | { ChmodClass.Group, Permissions.None }, 32 | { ChmodClass.Other, Permissions.None }, 33 | }; 34 | 35 | foreach (var token in pattern) 36 | { 37 | var current = queue.Dequeue(); 38 | if (char.IsNumber(token)) 39 | { 40 | var n = int.Parse(token.ToString()); 41 | map[current] = (Permissions)n; 42 | } 43 | } 44 | 45 | return new Chmod( 46 | mode, 47 | map[ChmodClass.Owner], 48 | map[ChmodClass.Group], 49 | map[ChmodClass.Other]); 50 | } 51 | 52 | private static SpecialMode ParseSpecialMode(char token) 53 | { 54 | if (char.IsNumber(token)) 55 | { 56 | return (SpecialMode)int.Parse(token.ToString()); 57 | } 58 | 59 | return SpecialMode.None; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/ChmodFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cupboard.Internal 4 | { 5 | internal static class ChmodFormatter 6 | { 7 | public static string Format(Chmod chmod, ChmodFormatting formatting) 8 | { 9 | return formatting switch 10 | { 11 | ChmodFormatting.Numeric => FormatNumeric(chmod), 12 | ChmodFormatting.Symbolic => FormatVerbose(chmod), 13 | _ => throw new NotSupportedException("Formatting option is not supported."), 14 | }; 15 | } 16 | 17 | private static string FormatNumeric(Chmod chmod) 18 | { 19 | return string.Concat( 20 | ((int)chmod.Mode).ToString(), 21 | Format(chmod.Owner, ChmodFormatting.Numeric), 22 | Format(chmod.Group, ChmodFormatting.Numeric), 23 | Format(chmod.Other, ChmodFormatting.Numeric)); 24 | } 25 | 26 | private static string FormatVerbose(Chmod chmod) 27 | { 28 | return string.Concat( 29 | Format(chmod.Owner, ChmodFormatting.Symbolic), 30 | Format(chmod.Group, ChmodFormatting.Symbolic), 31 | Format(chmod.Other, ChmodFormatting.Symbolic)); 32 | } 33 | 34 | private static string Format(Permissions permissions, ChmodFormatting formatting) 35 | { 36 | if (formatting == ChmodFormatting.Numeric) 37 | { 38 | return ((int)permissions).ToString(); 39 | } 40 | 41 | if (formatting == ChmodFormatting.Symbolic) 42 | { 43 | return string.Concat( 44 | (permissions & Permissions.Read) == Permissions.Read ? "r" : "-", 45 | (permissions & Permissions.Write) == Permissions.Write ? "w" : "-", 46 | (permissions & Permissions.Execute) == Permissions.Execute ? "x" : "-"); 47 | } 48 | 49 | throw new NotSupportedException("Formatting option is not supported."); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Cupboard/EnvironmentRefresher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using Microsoft.Win32; 5 | 6 | namespace Cupboard.Internal 7 | { 8 | internal class EnvironmentRefresher : IEnvironmentRefresher 9 | { 10 | public void Refresh() 11 | { 12 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 13 | { 14 | return; 15 | } 16 | 17 | var roots = new[] 18 | { 19 | Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"), 20 | Registry.CurrentUser.OpenSubKey("Environment"), 21 | }; 22 | 23 | var result = new Dictionary(StringComparer.OrdinalIgnoreCase); 24 | 25 | foreach (var root in roots) 26 | { 27 | if (root == null) 28 | { 29 | continue; 30 | } 31 | 32 | var keys = root.GetValueNames(); 33 | if (keys != null) 34 | { 35 | foreach (var key in keys) 36 | { 37 | if (key.Equals("PATH", StringComparison.OrdinalIgnoreCase)) 38 | { 39 | result.TryGetValue(key, out var path); 40 | path ??= string.Empty; 41 | result[key] = (path + ";" + root.GetValue(key)?.ToString() ?? string.Empty).TrimStart(';'); 42 | } 43 | else 44 | { 45 | result[key] = root.GetValue(key)?.ToString() ?? string.Empty; 46 | } 47 | } 48 | } 49 | } 50 | 51 | foreach (var envVar in result) 52 | { 53 | System.Environment.SetEnvironmentVariable(envVar.Key, envVar.Value, EnvironmentVariableTarget.Process); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Extensions/CupboardFixtureExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Spectre.IO; 3 | 4 | namespace Cupboard.Testing 5 | { 6 | public static class CupboardFixtureExtensions 7 | { 8 | public static void FileShouldExist(this CupboardFixture fixture, FilePath path) 9 | { 10 | if (fixture is null) 11 | { 12 | throw new ArgumentNullException(nameof(fixture)); 13 | } 14 | 15 | if (path is null) 16 | { 17 | throw new ArgumentNullException(nameof(path)); 18 | } 19 | 20 | if (!fixture.FileSystem.File.Exists(path)) 21 | { 22 | throw new InvalidOperationException($"File at {path.FullPath} does not exist"); 23 | } 24 | } 25 | 26 | public static void FileShouldNotExist(this CupboardFixture fixture, FilePath path) 27 | { 28 | if (fixture is null) 29 | { 30 | throw new ArgumentNullException(nameof(fixture)); 31 | } 32 | 33 | if (path is null) 34 | { 35 | throw new ArgumentNullException(nameof(path)); 36 | } 37 | 38 | if (fixture.FileSystem.File.Exists(path)) 39 | { 40 | throw new InvalidOperationException($"File at {path.FullPath} should not exist"); 41 | } 42 | } 43 | 44 | public static void FileShouldBeSymbolicLink(this CupboardFixture fixture, FilePath path) 45 | { 46 | if (fixture is null) 47 | { 48 | throw new ArgumentNullException(nameof(fixture)); 49 | } 50 | 51 | if (path is null) 52 | { 53 | throw new ArgumentNullException(nameof(path)); 54 | } 55 | 56 | FileShouldExist(fixture, path); 57 | 58 | if (fixture.FileSystem.GetFakeFile(path)?.SymbolicLink == null) 59 | { 60 | throw new InvalidOperationException($"File at {path.FullPath} is not a symbolic link"); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Cupboard.Core/FactCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard 5 | { 6 | public sealed class FactCollection : IEnumerable 7 | { 8 | private readonly Fact _root; 9 | 10 | public Fact this[string key] => _root[key]; 11 | 12 | public FactCollection() 13 | { 14 | _root = new Fact(null, string.Empty, null); 15 | } 16 | 17 | public void Add(string key, object value) 18 | { 19 | var root = _root; 20 | 21 | var queue = new Queue(key.Split('.')); 22 | while (queue.Count > 0) 23 | { 24 | var current = queue.Dequeue(); 25 | 26 | if (!root.Children.TryGetValue(current, out var node)) 27 | { 28 | node = queue.Count == 0 29 | ? new Fact(root, current, value) 30 | : new Fact(root, current); 31 | 32 | root.Children.Add(current, node); 33 | } 34 | else 35 | { 36 | if (queue.Count == 0) 37 | { 38 | // Overwrite the value 39 | node.Value = value; 40 | } 41 | } 42 | 43 | root = node; 44 | } 45 | } 46 | 47 | public IEnumerator GetEnumerator() 48 | { 49 | var stack = new Stack(); 50 | stack.Push(_root); 51 | while (stack.Count > 0) 52 | { 53 | var current = stack.Pop(); 54 | if (current.Value != null) 55 | { 56 | yield return current; 57 | } 58 | 59 | foreach (var (_, child) in current.Children) 60 | { 61 | stack.Push(child); 62 | } 63 | } 64 | } 65 | 66 | IEnumerator IEnumerable.GetEnumerator() 67 | { 68 | return GetEnumerator(); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Cupboard/IO/WindowsRegistryKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using Win32RegistryKey = Microsoft.Win32.RegistryKey; 5 | 6 | namespace Cupboard.Internal 7 | { 8 | [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] 9 | internal sealed class WindowsRegistryKey : IWindowsRegistryKey 10 | { 11 | private readonly Win32RegistryKey _key; 12 | 13 | public WindowsRegistryKey(Win32RegistryKey key) 14 | { 15 | _key = key ?? throw new ArgumentNullException(nameof(key)); 16 | } 17 | 18 | public IWindowsRegistryKey? CreateSubKey(string name, bool writable) 19 | { 20 | var key = _key.CreateSubKey(name, writable); 21 | return new WindowsRegistryKey(key); 22 | } 23 | 24 | public int GetValueCount() 25 | { 26 | return _key.ValueCount; 27 | } 28 | 29 | public void DeleteValue(string name) 30 | { 31 | _key.DeleteValue(name); 32 | } 33 | 34 | public bool ValueExists(string name) 35 | { 36 | return _key.GetValueNames().Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase)); 37 | } 38 | 39 | public object? GetValue(string name) 40 | { 41 | return _key.GetValue(name, null); 42 | } 43 | 44 | public IWindowsRegistryKey? OpenSubKey(string name, bool writable) 45 | { 46 | var key = _key.OpenSubKey(name, writable); 47 | if (key == null) 48 | { 49 | return null; 50 | } 51 | 52 | return new WindowsRegistryKey(key); 53 | } 54 | 55 | [Obsolete("Please use SetValue overload accepting a RegistryValueKind instead")] 56 | public void SetValue(string name, object value, RegistryKeyValueKind kind) 57 | { 58 | _key.SetValue(name, value, kind.ToWin32()); 59 | } 60 | 61 | public void SetValue(string name, object value, RegistryValueKind kind) 62 | { 63 | _key.SetValue(name, value, kind.ToWin32()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | branches: 8 | - main 9 | 10 | env: 11 | # Set the DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable to stop wasting time caching packages 12 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 13 | # Disable sending usage data to Microsoft 14 | DOTNET_CLI_TELEMETRY_OPTOUT: true 15 | 16 | jobs: 17 | 18 | ################################################### 19 | # BUILD 20 | ################################################### 21 | 22 | build: 23 | name: Build 24 | if: "!contains(github.event.head_commit.message, 'skip-ci')" 25 | strategy: 26 | matrix: 27 | kind: ['linux', 'windows', 'macOS'] 28 | include: 29 | - kind: linux 30 | os: ubuntu-latest 31 | - kind: windows 32 | os: windows-latest 33 | - kind: macOS 34 | os: macos-latest 35 | runs-on: ${{ matrix.os }} 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v2 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: 'Get Git tags' 43 | run: git fetch --tags 44 | shell: bash 45 | 46 | - name: Setup dotnet 47 | uses: actions/setup-dotnet@v1 48 | with: 49 | dotnet-version: 5.0.301 50 | 51 | - name: Build 52 | shell: bash 53 | run: | 54 | dotnet tool restore 55 | dotnet cake 56 | 57 | ################################################### 58 | # PUBLISH 59 | ################################################### 60 | 61 | publish: 62 | name: Publish 63 | needs: [build] 64 | if: "!contains(github.event.head_commit.message, 'skip-ci')" 65 | runs-on: windows-latest 66 | steps: 67 | - name: Checkout 68 | uses: actions/checkout@v2 69 | with: 70 | fetch-depth: 0 71 | 72 | - name: 'Get Git tags' 73 | run: git fetch --tags 74 | shell: bash 75 | 76 | - name: Setup dotnet 5.0.301 77 | uses: actions/setup-dotnet@v1 78 | with: 79 | dotnet-version: 5.0.301 80 | 81 | - name: Publish 82 | shell: bash 83 | run: | 84 | dotnet tool restore 85 | dotnet cake --target="publish" \ 86 | --nuget-key="${{secrets.NUGET_API_KEY}}" -------------------------------------------------------------------------------- /src/Cupboard/ResourceGraphBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | internal static class ResourceGraphBuilder 7 | { 8 | public static ResourceGraph Build( 9 | ResourceProviderRepository repository, 10 | IEnumerable manifests, 11 | FactCollection facts) 12 | { 13 | // Execute all manifests. 14 | var ctx = new ManifestContext(facts); 15 | foreach (var manifest in manifests) 16 | { 17 | manifest.Execute(ctx); 18 | } 19 | 20 | // Build the graph using the builders in the context. 21 | var graph = new ResourceGraph(); 22 | foreach (var builder in ctx.Builders) 23 | { 24 | var provider = repository.GetProvider(builder.Type); 25 | if (provider == null) 26 | { 27 | throw new InvalidOperationException($"Could not find resource provider for '{builder.Name}' ({builder.Type.Name})."); 28 | } 29 | 30 | // Create the resource. 31 | var resource = provider.Create(builder.Name); 32 | 33 | // Add the resource to the graph. 34 | if (!graph.Resources.Add(resource)) 35 | { 36 | // TODO 2021-07-11: Log that the resource was skipped 37 | continue; 38 | } 39 | 40 | // Connect dependees to resource. 41 | foreach (var before in builder.RunBefore) 42 | { 43 | graph.Connect(new ResourceIdentity(resource), new ResourceIdentity(before)); 44 | } 45 | 46 | // Connect resource to dependencies. 47 | foreach (var after in builder.RunAfter) 48 | { 49 | graph.Connect(new ResourceIdentity(after), new ResourceIdentity(resource)); 50 | } 51 | 52 | // Run configuration. 53 | foreach (var configuration in builder.Configurations) 54 | { 55 | graph.Configurations.Add(() => configuration(resource)); 56 | } 57 | } 58 | 59 | // Return the graph. 60 | return graph; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Cupboard.Tests/Unit/IO/RegistryKeyTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using Xunit; 3 | 4 | namespace Cupboard.Tests.Unit.IO 5 | { 6 | public sealed class RegistryKeyPathTests 7 | { 8 | [Fact] 9 | public void Should_Not_Consider_Path_Missing_Key_Valid() 10 | { 11 | // Given, When 12 | var path = new RegistryPath("HKEY_CURRENT_USER"); 13 | 14 | // Then 15 | path.IsValid.ShouldBeFalse(); 16 | } 17 | 18 | [Fact] 19 | public void Should_Not_Consider_Path_Missing_Sub_Key_Valid() 20 | { 21 | // Given, When 22 | var path = new RegistryPath("HKEY_CURRENT_USER\\Foo"); 23 | 24 | // Then 25 | path.IsValid.ShouldBeFalse(); 26 | } 27 | 28 | [Fact] 29 | public void Should_Normalize_Separators() 30 | { 31 | // Given, When 32 | var path = new RegistryPath("HKEY_CURRENT_USER/Foo/Bar/Baz"); 33 | 34 | // Then 35 | path.ToString().ShouldBe(@"HKEY_CURRENT_USER\Foo\Bar\Baz"); 36 | path.Path.ShouldBe(@"Foo\Bar\Baz"); 37 | path.IsValid.ShouldBeTrue(); 38 | } 39 | 40 | [Theory] 41 | [InlineData("HKCR/Foo/Bar/Baz", RegistryHive.ClassesRoot)] 42 | [InlineData("HKCU/Foo/Bar/Baz", RegistryHive.CurrentUser)] 43 | [InlineData("HKLM/Foo/Bar/Baz", RegistryHive.LocalMachine)] 44 | [InlineData("HKU/Foo/Bar/Baz", RegistryHive.Users)] 45 | [InlineData("HKPD/Foo/Bar/Baz", RegistryHive.PerformanceData)] 46 | [InlineData("HKCC/Foo/Bar/Baz", RegistryHive.CurrentConfig)] 47 | [InlineData("HKCR:/Foo/Bar/Baz", RegistryHive.ClassesRoot)] 48 | [InlineData("HKCU:/Foo/Bar/Baz", RegistryHive.CurrentUser)] 49 | [InlineData("HKLM:/Foo/Bar/Baz", RegistryHive.LocalMachine)] 50 | [InlineData("HKU:/Foo/Bar/Baz", RegistryHive.Users)] 51 | [InlineData("HKPD:/Foo/Bar/Baz", RegistryHive.PerformanceData)] 52 | [InlineData("HKCC:/Foo/Bar/Baz", RegistryHive.CurrentConfig)] 53 | public void Should_Substitute_Roots(string path, RegistryHive expected) 54 | { 55 | // Given, When 56 | var result = new RegistryPath(path); 57 | 58 | // Then 59 | result.IsValid.ShouldBeTrue(); 60 | result.Hive.ShouldBe(expected); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Cupboard.Tests/Unit/IO/ChmodParserTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Cupboard.Tests.Unit.IO 4 | { 5 | public sealed class ChmodParserTests 6 | { 7 | public sealed class TheParseMethod 8 | { 9 | [Theory] 10 | [InlineData("000", SpecialMode.None, Permissions.None, Permissions.None, Permissions.None)] 11 | [InlineData("500", SpecialMode.None, Permissions.Read | Permissions.Execute, Permissions.None, Permissions.None)] 12 | [InlineData("050", SpecialMode.None, Permissions.None, Permissions.Read | Permissions.Execute, Permissions.None)] 13 | [InlineData("005", SpecialMode.None, Permissions.None, Permissions.None, Permissions.Read | Permissions.Execute)] 14 | [InlineData("777", SpecialMode.None, Permissions.All, Permissions.All, Permissions.All)] 15 | [InlineData("0000", SpecialMode.None, Permissions.None, Permissions.None, Permissions.None)] 16 | [InlineData("0500", SpecialMode.None, Permissions.Read | Permissions.Execute, Permissions.None, Permissions.None)] 17 | [InlineData("0050", SpecialMode.None, Permissions.None, Permissions.Read | Permissions.Execute, Permissions.None)] 18 | [InlineData("0005", SpecialMode.None, Permissions.None, Permissions.None, Permissions.Read | Permissions.Execute)] 19 | [InlineData("0777", SpecialMode.None, Permissions.All, Permissions.All, Permissions.All)] 20 | [InlineData("1000", SpecialMode.Sticky, Permissions.None, Permissions.None, Permissions.None)] 21 | [InlineData("2000", SpecialMode.Setgid, Permissions.None, Permissions.None, Permissions.None)] 22 | [InlineData("4000", SpecialMode.Setuid, Permissions.None, Permissions.None, Permissions.None)] 23 | [InlineData("7000", SpecialMode.Sticky | SpecialMode.Setgid | SpecialMode.Setuid, Permissions.None, Permissions.None, Permissions.None)] 24 | public void Should_Return_Correct_Permissions(string pattern, SpecialMode mode, Permissions owner, Permissions group, Permissions other) 25 | { 26 | // Given, When 27 | var result = ChmodParser.Parse(pattern); 28 | 29 | // Then 30 | Assert.Equal(mode, result.Mode); 31 | Assert.Equal(owner, result.Owner); 32 | Assert.Equal(group, result.Group); 33 | Assert.Equal(other, result.Other); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Chocolatey/ChocolateyPackageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Cupboard 5 | { 6 | public sealed class ChocolateyPackageProvider : PackageInstallerProvider 7 | { 8 | private readonly IProcessRunner _runner; 9 | 10 | protected override string Name { get; } = "Chocolatey"; 11 | 12 | public ChocolateyPackageProvider( 13 | IProcessRunner runner, 14 | ICupboardLogger logger, 15 | IEnvironmentRefresher refresher) 16 | : base(logger, refresher) 17 | { 18 | _runner = runner ?? throw new ArgumentNullException(nameof(runner)); 19 | } 20 | 21 | public override ChocolateyPackage Create(string name) 22 | { 23 | return new ChocolateyPackage(name) 24 | { 25 | Package = name, 26 | }; 27 | } 28 | 29 | protected override bool IsPackageInstalled(ChocolateyPackage resource, string output) 30 | { 31 | return output.Contains(resource.Package, StringComparison.OrdinalIgnoreCase); 32 | } 33 | 34 | protected override async Task GetPackageState(ChocolateyPackage resource) 35 | { 36 | return await _runner.Run("choco", "list -lo", supressOutput: true).ConfigureAwait(false); 37 | } 38 | 39 | protected override async Task InstallPackage(ChocolateyPackage resource) 40 | { 41 | var arguments = $"install {resource.Package} -y"; 42 | 43 | if (resource.PreRelease) 44 | { 45 | arguments += " --pre"; 46 | } 47 | 48 | if (resource.IgnoreChecksum) 49 | { 50 | arguments += " --ignore-checksum"; 51 | } 52 | 53 | if (!string.IsNullOrWhiteSpace(resource.PackageParameters)) 54 | { 55 | arguments += $" --package-parameters=\"{resource.PackageParameters}\""; 56 | } 57 | 58 | return await _runner.Run("choco", arguments, t => !t.StartsWith("Progress:")).ConfigureAwait(false); 59 | } 60 | 61 | protected override async Task UninstallPackage(ChocolateyPackage resource) 62 | { 63 | return await _runner.Run("choco", $"uninstall {resource.Package}").ConfigureAwait(false); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Cupboard.Testing 6 | { 7 | public sealed class FakeLogger : ICupboardLogger 8 | { 9 | private readonly List _messages; 10 | 11 | public Verbosity Verbosity { get; private set; } = Verbosity.Normal; 12 | 13 | private sealed class LoggedMessage 14 | { 15 | public Verbosity Verbosity { get; init; } 16 | public LogLevel Level { get; init; } 17 | public string Message { get; init; } 18 | 19 | public LoggedMessage() 20 | { 21 | Message = string.Empty; 22 | } 23 | } 24 | 25 | public FakeLogger() 26 | { 27 | _messages = new List(); 28 | } 29 | 30 | public void Log(Verbosity verbosity, LogLevel level, string text) 31 | { 32 | _messages.Add(new LoggedMessage 33 | { 34 | Verbosity = verbosity, 35 | Level = level, 36 | Message = text, 37 | }); 38 | } 39 | 40 | public void Log(Verbosity verbosity, LogLevel level, string title, string text) 41 | { 42 | _messages.Add(new LoggedMessage 43 | { 44 | Verbosity = verbosity, 45 | Level = level, 46 | Message = title + " " + text, 47 | }); 48 | } 49 | 50 | public void SetVerbosity(Verbosity verbosity) 51 | { 52 | Verbosity = verbosity; 53 | } 54 | 55 | public void WasLogged(string message, Verbosity? verbosity = null, LogLevel? level = null) 56 | { 57 | var messages = _messages.Where(x => x.Message.Equals(message, StringComparison.Ordinal)); 58 | 59 | if (verbosity != null) 60 | { 61 | messages = messages.Where(x => x.Verbosity == verbosity.Value); 62 | } 63 | 64 | if (level != null) 65 | { 66 | messages = messages.Where(x => x.Level == level.Value); 67 | } 68 | 69 | if (!messages.Any()) 70 | { 71 | var list = string.Join("\n", _messages.Select(m => $"* {m.Message}")); 72 | throw new InvalidOperationException($"Received no log calls that matched.\nReceived messages:\n\n{list}"); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Cupboard/IO/ProcessRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading.Tasks; 4 | using CliWrap; 5 | using CliWrap.EventStream; 6 | using Spectre.Console; 7 | 8 | namespace Cupboard.Internal 9 | { 10 | internal sealed class ProcessRunner : IProcessRunner 11 | { 12 | private readonly ICupboardLogger _logger; 13 | 14 | public ProcessRunner(ICupboardLogger logger) 15 | { 16 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 17 | } 18 | 19 | public async Task Run(string file, string? arguments = null, Func? filter = null, bool supressOutput = false) 20 | { 21 | var cli = Cli.Wrap(file); 22 | cli = cli.WithValidation(CommandResultValidation.None); 23 | 24 | if (arguments != null) 25 | { 26 | cli = cli.WithArguments(arguments); 27 | } 28 | 29 | var standardOut = new StringBuilder(); 30 | var standardError = new StringBuilder(); 31 | var exitCode = -1; 32 | 33 | await foreach (var cmdEvent in cli.ListenAsync()) 34 | { 35 | switch (cmdEvent) 36 | { 37 | case StandardOutputCommandEvent output: 38 | standardOut.Append(output.Text); 39 | if (!supressOutput && !string.IsNullOrWhiteSpace(output.Text) && (filter?.Invoke(output.Text) ?? true)) 40 | { 41 | _logger.Verbose("OUT>", output.Text.EscapeMarkup().TrimStart()); 42 | } 43 | 44 | break; 45 | case StandardErrorCommandEvent error: 46 | if (!supressOutput && !string.IsNullOrWhiteSpace(error.Text) && (filter?.Invoke(error.Text) ?? true)) 47 | { 48 | _logger.Error("ERR>", error.Text.EscapeMarkup().TrimStart()); 49 | } 50 | 51 | standardError.Append(error.Text); 52 | break; 53 | case ExitedCommandEvent exited: 54 | exitCode = exited.ExitCode; 55 | break; 56 | } 57 | } 58 | 59 | return new ProcessRunnerResult( 60 | exitCode, 61 | standardOut.ToString(), 62 | standardError.ToString()); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Extensions/ICupboardLoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Cupboard 2 | { 3 | public static class ICupboardLoggerExtensions 4 | { 5 | public static void Fatal(this ICupboardLogger logger, string markup) 6 | { 7 | logger?.Log(Verbosity.Quiet, LogLevel.Fatal, markup); 8 | } 9 | 10 | public static void Error(this ICupboardLogger logger, string markup) 11 | { 12 | logger?.Log(Verbosity.Quiet, LogLevel.Error, markup); 13 | } 14 | 15 | public static void Warning(this ICupboardLogger logger, string markup) 16 | { 17 | logger?.Log(Verbosity.Minimal, LogLevel.Warning, markup); 18 | } 19 | 20 | public static void Information(this ICupboardLogger logger, string markup) 21 | { 22 | logger?.Log(Verbosity.Normal, LogLevel.Information, markup); 23 | } 24 | 25 | public static void Verbose(this ICupboardLogger logger, string markup) 26 | { 27 | logger?.Log(Verbosity.Verbose, LogLevel.Verbose, markup); 28 | } 29 | 30 | public static void Debug(this ICupboardLogger logger, string markup) 31 | { 32 | logger?.Log(Verbosity.Diagnostic, LogLevel.Debug, markup); 33 | } 34 | 35 | public static void Fatal(this ICupboardLogger logger, string title, string markup) 36 | { 37 | logger?.Log(Verbosity.Quiet, LogLevel.Fatal, title, markup); 38 | } 39 | 40 | public static void Error(this ICupboardLogger logger, string title, string markup) 41 | { 42 | logger?.Log(Verbosity.Quiet, LogLevel.Error, title, markup); 43 | } 44 | 45 | public static void Warning(this ICupboardLogger logger, string title, string markup) 46 | { 47 | logger?.Log(Verbosity.Minimal, LogLevel.Warning, title, markup); 48 | } 49 | 50 | public static void Information(this ICupboardLogger logger, string title, string markup) 51 | { 52 | logger?.Log(Verbosity.Normal, LogLevel.Information, title, markup); 53 | } 54 | 55 | public static void Verbose(this ICupboardLogger logger, string title, string markup) 56 | { 57 | logger?.Log(Verbosity.Verbose, LogLevel.Verbose, title, markup); 58 | } 59 | 60 | public static void Debug(this ICupboardLogger logger, string title, string markup) 61 | { 62 | logger?.Log(Verbosity.Diagnostic, LogLevel.Debug, title, markup); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 9.0 5 | true 6 | embedded 7 | true 8 | true 9 | false 10 | 11 | 12 | 13 | true 14 | 15 | 16 | 17 | A framework for provisioning local environments to a desired state, using the .NET SDK. 18 | Patrik Svensson 19 | Patrik Svensson 20 | git 21 | https://github.com/patriksvensson/cupboard 22 | True 23 | https://github.com/patriksvensson/cupboard 24 | MIT 25 | https://github.com/patriksvensson/cupboard/releases 26 | 27 | 28 | 29 | true 30 | true 31 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 32 | 33 | 34 | 35 | 36 | 37 | 38 | all 39 | runtime; build; native; contentfiles; analyzers; buildtransitive 40 | 41 | 42 | All 43 | 44 | 45 | All 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/Exec/ExecProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Spectre.IO; 5 | 6 | namespace Cupboard 7 | { 8 | public sealed class ExecProvider : AsyncResourceProvider 9 | { 10 | private readonly IProcessRunner _runner; 11 | private readonly ICupboardFileSystem _fileSystem; 12 | private readonly ICupboardEnvironment _environment; 13 | private readonly IEnvironmentRefresher _refresher; 14 | private readonly ICupboardLogger _logger; 15 | 16 | public ExecProvider( 17 | IProcessRunner runner, 18 | ICupboardFileSystem fileSystem, 19 | ICupboardEnvironment environment, 20 | IEnvironmentRefresher refresher, 21 | ICupboardLogger logger) 22 | { 23 | _runner = runner ?? throw new ArgumentNullException(nameof(runner)); 24 | _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); 25 | _environment = environment ?? throw new ArgumentNullException(nameof(environment)); 26 | _refresher = refresher ?? throw new ArgumentNullException(nameof(refresher)); 27 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 28 | } 29 | 30 | public override Exec Create(string name) 31 | { 32 | return new Exec(name); 33 | } 34 | 35 | public override async Task RunAsync(IExecutionContext context, Exec resource) 36 | { 37 | var args = resource.Args ?? string.Empty; 38 | 39 | var path = resource.Path.MakeAbsolute(_environment); 40 | if (!_fileSystem.Exist(path)) 41 | { 42 | _logger.Error($"The file {path.FullPath} could not be found"); 43 | return ResourceState.Error; 44 | } 45 | 46 | if (context.DryRun) 47 | { 48 | return ResourceState.Unknown; 49 | } 50 | 51 | var result = await _runner.Run(path.FullPath, args).ConfigureAwait(false); 52 | if (result.ExitCode != 0) 53 | { 54 | if (resource.ValidExitCodes?.Contains(result.ExitCode) == true) 55 | { 56 | _refresher.Refresh(); 57 | return ResourceState.Changed; 58 | } 59 | 60 | _logger.Error($"The file {path.FullPath} returned exit code {result.ExitCode}"); 61 | return ResourceState.Error; 62 | } 63 | 64 | return ResourceState.Changed; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Cupboard.Providers.Windows/Winget/WingetPackageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Cupboard 5 | { 6 | public sealed class WingetPackageProvider : PackageInstallerProvider 7 | { 8 | private readonly IProcessRunner _runner; 9 | 10 | protected override string Name { get; } = "Winget"; 11 | 12 | public WingetPackageProvider( 13 | IProcessRunner runner, 14 | ICupboardLogger logger, 15 | IEnvironmentRefresher refresher) 16 | : base(logger, refresher) 17 | { 18 | _runner = runner ?? throw new ArgumentNullException(nameof(runner)); 19 | } 20 | 21 | public override WingetPackage Create(string name) 22 | { 23 | return new WingetPackage(name) 24 | { 25 | Package = name, 26 | }; 27 | } 28 | 29 | protected override bool IsPackageInstalled(WingetPackage resource, string output) 30 | { 31 | return output.Contains(resource.Package, StringComparison.OrdinalIgnoreCase); 32 | } 33 | 34 | protected override async Task GetPackageState(WingetPackage resource) 35 | { 36 | var arguments = $"list --source winget --id {resource.Package}"; 37 | return await _runner.Run("winget", arguments, supressOutput: true).ConfigureAwait(false); 38 | } 39 | 40 | protected override bool IsError(PackageInstallerOperation operation, ProcessRunnerResult result) 41 | { 42 | if (operation == PackageInstallerOperation.RetriveState) 43 | { 44 | return result.ExitCode != 0 && (!result.StandardOut?.EndsWith("No installed package found matching input criteria.") ?? true); 45 | } 46 | 47 | return result.ExitCode != 0; 48 | } 49 | 50 | protected override async Task InstallPackage(WingetPackage resource) 51 | { 52 | var arguments = $"install -e --id {resource.Package}"; 53 | 54 | if (resource.Force) 55 | { 56 | arguments += " --force"; 57 | } 58 | 59 | if (!string.IsNullOrWhiteSpace(resource.PackageVersion)) 60 | { 61 | arguments += $" --version {resource.PackageVersion}"; 62 | } 63 | 64 | return await _runner.Run("winget", arguments).ConfigureAwait(false); 65 | } 66 | 67 | protected override async Task UninstallPackage(WingetPackage resource) 68 | { 69 | return await _runner.Run("winget", $"uninstall -e --id {resource.Package}").ConfigureAwait(false); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Sandbox/Manifests/WindowsSettings.cs: -------------------------------------------------------------------------------- 1 | using Cupboard; 2 | 3 | namespace Sandbox 4 | { 5 | public sealed class WindowsSettings : Manifest 6 | { 7 | public override void Execute(ManifestContext context) 8 | { 9 | if (!context.Facts["windows"]["sandbox"]) 10 | { 11 | context.Resource("Windows Sandbox") 12 | .FeatureName("Containers-DisposableClientVM") 13 | .Ensure(WindowsFeatureState.Enabled); 14 | } 15 | 16 | context.Resource("Disable Game bar tips") 17 | .Path(@"HKCU:\SOFTWARE\Microsoft\GameBar") 18 | .Value("ShowStartupPanel") 19 | .Data(0, RegistryValueKind.DWord); 20 | 21 | context.Resource("Disable Bing suggestions") 22 | .Path(@"HKCU:\Software\Policies\Microsoft\Windows\Explorer") 23 | .Value("DisableSearchBoxSuggestions") 24 | .Data(1, RegistryValueKind.DWord); 25 | 26 | context.Resource("Disable Bing search") 27 | .Path(@"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Search") 28 | .Value("BingSearchEnabled") 29 | .Data(0, RegistryValueKind.DWord); 30 | 31 | context.Resource("Disable lock screen") 32 | .Path(@"HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization") 33 | .Value("NoLockScreen") 34 | .Data(1, RegistryValueKind.DWord); 35 | 36 | context.Resource("Disable People in taskbar") 37 | .Path(@"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\People") 38 | .Value("PeopleBand") 39 | .Ensure(RegistryKeyState.DoNotExist); 40 | 41 | // File Explorer options 42 | context.Resource("Show file extensions in File Explorer.") 43 | .Path(@"HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced") 44 | .Value("HideFileExt") 45 | .Data(0, RegistryValueKind.DWord); 46 | 47 | context.Resource("Show hidden files in File Explorer") 48 | .Path(@"HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced") 49 | .Value("Hidden") 50 | .Data(1, RegistryValueKind.DWord); 51 | 52 | context.Resource("Show full path in File Explorer title bar") 53 | .Path(@"HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\CabinetState") 54 | .Value("FullPathAddress") 55 | .Data(1, RegistryValueKind.DWord); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Cupboard/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using Cupboard.Internal; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Spectre.Console.Cli; 7 | 8 | namespace Cupboard 9 | { 10 | public static class ServiceCollectionExtensions 11 | { 12 | public static IServiceCollection AddModule(this IServiceCollection services) 13 | where T : ServiceModule, new() 14 | { 15 | if (services is null) 16 | { 17 | throw new ArgumentNullException(nameof(services)); 18 | } 19 | 20 | var module = new T(); 21 | module.Configure(services); 22 | return services; 23 | } 24 | 25 | internal static IServiceCollection AddAll(this IServiceCollection services, Assembly? assembly = null) 26 | { 27 | var serviceType = typeof(TService); 28 | var isInterface = serviceType.IsInterface; 29 | 30 | var assemblies = assembly != null 31 | ? new Assembly[] { assembly } 32 | : AppDomain.CurrentDomain.GetAssemblies(); 33 | 34 | foreach (var type in assemblies.SelectMany(assembly => assembly.GetTypes())) 35 | { 36 | if (isInterface && type.IsInterface) 37 | { 38 | continue; 39 | } 40 | 41 | if (!type.IsAbstract && serviceType.IsAssignableFrom(type)) 42 | { 43 | if (!services.Any(x => x.ImplementationType == type)) 44 | { 45 | services.AddSingleton(serviceType, type); 46 | } 47 | } 48 | } 49 | 50 | return services; 51 | } 52 | 53 | internal static IServiceCollection AddCommandLine( 54 | this IServiceCollection services, 55 | Action configurator) 56 | { 57 | var app = new CommandApp(new TypeRegistrar(services)); 58 | app.Configure(configurator); 59 | services.AddSingleton(app); 60 | 61 | return services; 62 | } 63 | 64 | internal static IServiceCollection AddCommandLine( 65 | this IServiceCollection services, 66 | Action? configurator = null) 67 | where TCommand : class, ICommand 68 | { 69 | var app = new CommandApp(new TypeRegistrar(services)); 70 | 71 | if (configurator != null) 72 | { 73 | app.Configure(configurator); 74 | } 75 | 76 | services.AddSingleton(app); 77 | 78 | return services; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Cupboard/CupboardLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Spectre.Console; 3 | 4 | namespace Cupboard.Internal 5 | { 6 | internal sealed class CupboardLogger : ICupboardLogger 7 | { 8 | private readonly IAnsiConsole _console; 9 | private Verbosity _verbosity; 10 | 11 | public Verbosity Verbosity => _verbosity; 12 | 13 | public CupboardLogger(IAnsiConsole console) 14 | { 15 | _console = console ?? throw new ArgumentNullException(nameof(console)); 16 | _verbosity = Verbosity.Normal; 17 | } 18 | 19 | public void SetVerbosity(Verbosity verbosity) 20 | { 21 | _verbosity = verbosity; 22 | } 23 | 24 | public void Log(Verbosity verbosity, LogLevel level, string text) 25 | { 26 | if (verbosity > _verbosity) 27 | { 28 | return; 29 | } 30 | 31 | switch (level) 32 | { 33 | case LogLevel.Fatal: 34 | case LogLevel.Error: 35 | _console.MarkupLine($"[red]{text}[/]"); 36 | break; 37 | case LogLevel.Warning: 38 | _console.MarkupLine($"[yellow]{text}[/]"); 39 | break; 40 | case LogLevel.Information: 41 | _console.MarkupLine($"{text}"); 42 | break; 43 | case LogLevel.Verbose: 44 | case LogLevel.Debug: 45 | _console.MarkupLine($"[grey]{text}[/]"); 46 | break; 47 | } 48 | } 49 | 50 | public void Log(Verbosity verbosity, LogLevel level, string title, string text) 51 | { 52 | if (verbosity > _verbosity) 53 | { 54 | return; 55 | } 56 | 57 | switch (level) 58 | { 59 | case LogLevel.Fatal: 60 | case LogLevel.Error: 61 | WriteGrid($"[red]{title}[/]", $"[red]{text}[/]"); 62 | break; 63 | case LogLevel.Warning: 64 | WriteGrid($"[yellow]{title}[/]", $"[red]{text}[/]"); 65 | break; 66 | case LogLevel.Information: 67 | WriteGrid(title, text); 68 | break; 69 | case LogLevel.Verbose: 70 | case LogLevel.Debug: 71 | WriteGrid($"[grey]{title}[/]", $"[grey]{text}[/]"); 72 | break; 73 | } 74 | } 75 | 76 | private void WriteGrid(string title, string text) 77 | { 78 | _console.Write(new Grid() 79 | .AddColumn(new GridColumn().PadRight(1)) 80 | .AddColumn() 81 | .AddRow(title, text)); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/VSCode/VSCodeExtensionProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Spectre.IO; 4 | 5 | namespace Cupboard 6 | { 7 | public sealed class VSCodeExtensionProvider : PackageInstallerProvider 8 | { 9 | private readonly IProcessRunner _runner; 10 | private readonly ICupboardEnvironment _environment; 11 | 12 | protected override string Name { get; } = "VSCode"; 13 | protected override string Kind { get; } = "extension"; 14 | 15 | public VSCodeExtensionProvider( 16 | IProcessRunner runner, 17 | ICupboardEnvironment environment, 18 | ICupboardLogger logger, 19 | IEnvironmentRefresher refresher) 20 | : base(logger, refresher) 21 | { 22 | _runner = runner ?? throw new ArgumentNullException(nameof(runner)); 23 | _environment = environment ?? throw new ArgumentNullException(nameof(environment)); 24 | } 25 | 26 | public override VSCodeExtension Create(string name) 27 | { 28 | return new VSCodeExtension(name) 29 | { 30 | Package = name, 31 | }; 32 | } 33 | 34 | protected override bool IsPackageInstalled(VSCodeExtension resource, string output) 35 | { 36 | return output.Contains(resource.Package, StringComparison.OrdinalIgnoreCase); 37 | } 38 | 39 | protected override async Task GetPackageState(VSCodeExtension resource) 40 | { 41 | var executable = GetCodeExecutable(); 42 | return await _runner.Run(executable, "--list-extensions", supressOutput: true).ConfigureAwait(false); 43 | } 44 | 45 | protected override async Task InstallPackage(VSCodeExtension resource) 46 | { 47 | var executable = GetCodeExecutable(); 48 | var arguments = $"--install-extension {resource.Package}"; 49 | return await _runner.Run(executable, arguments).ConfigureAwait(false); 50 | } 51 | 52 | protected override async Task UninstallPackage(VSCodeExtension resource) 53 | { 54 | var executable = GetCodeExecutable(); 55 | var arguments = $"--uninstall-extension {resource.Package}"; 56 | return await _runner.Run(executable, arguments).ConfigureAwait(false); 57 | } 58 | 59 | private string GetCodeExecutable() 60 | { 61 | if (_environment.Platform.Family == PlatformFamily.Windows) 62 | { 63 | // TODO 2021-07-14: This is not good 64 | return "C:/Program Files/Microsoft VS Code/bin/code.cmd"; 65 | } 66 | else 67 | { 68 | return "code"; 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Cupboard.Testing/Fakes/FakeProcessRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Cupboard.Testing 7 | { 8 | public sealed class FakeProcessRunner : IProcessRunner 9 | { 10 | private readonly Dictionary> _registrations; 11 | private readonly List<(string File, string Arguments)> _calls; 12 | private ProcessRunnerResult? _defaultRegistration; 13 | 14 | public FakeProcessRunner() 15 | { 16 | _registrations = new Dictionary>(StringComparer.Ordinal); 17 | _calls = new List<(string, string)>(); 18 | } 19 | 20 | public void RegisterDefaultResult(ProcessRunnerResult result) 21 | { 22 | _defaultRegistration = result; 23 | } 24 | 25 | public void Register(string file, string arguments, params ProcessRunnerResult[] results) 26 | { 27 | var key = GetKey(file, arguments); 28 | if (!_registrations.ContainsKey(key)) 29 | { 30 | _registrations[key] = new Queue(); 31 | } 32 | 33 | foreach (var result in results) 34 | { 35 | _registrations[key].Enqueue(result); 36 | } 37 | } 38 | 39 | public bool ReceivedCallToFile(string file) 40 | { 41 | return _calls.Any(c => c.File.Equals(file, StringComparison.OrdinalIgnoreCase)); 42 | } 43 | 44 | public Task Run(string file, string arguments, Func? filter = null, bool supressOutput = false) 45 | { 46 | var key = GetKey(file, arguments); 47 | if (_registrations.ContainsKey(key)) 48 | { 49 | if (_registrations[key].Count > 0) 50 | { 51 | _calls.Add((file, arguments)); 52 | return Task.FromResult(_registrations[key].Dequeue()); 53 | } 54 | } 55 | 56 | if (_defaultRegistration != null) 57 | { 58 | _calls.Add((file, arguments)); 59 | return Task.FromResult(_defaultRegistration); 60 | } 61 | 62 | var message = $"No process registration found for \"{key}\"."; 63 | var list = string.Join("\n", _calls.Select(m => $"* {m.File} {m.Arguments}")); 64 | if (!string.IsNullOrWhiteSpace(list)) 65 | { 66 | message += $"\nReceived calls:\n\n{list}"; 67 | } 68 | 69 | throw new InvalidOperationException(message); 70 | } 71 | 72 | private static string GetKey(string file, string arguments) 73 | { 74 | return $"{file} {arguments}"; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Cupboard/ResourceGraph.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Cupboard.Internal 6 | { 7 | internal sealed class ResourceGraph 8 | { 9 | private readonly IEqualityComparer _comparer; 10 | 11 | public HashSet Nodes { get; } 12 | public HashSet Edges { get; } 13 | public HashSet Resources { get; } 14 | public List Configurations { get; set; } 15 | 16 | public ResourceGraph() 17 | { 18 | _comparer = new ResourceComparer(); 19 | 20 | Nodes = new HashSet(); 21 | Edges = new HashSet(); 22 | Resources = new HashSet(_comparer); 23 | Configurations = new List(); 24 | } 25 | 26 | public void Add(Resource resource) 27 | { 28 | Resources.Add(resource); 29 | } 30 | 31 | public void Connect(IResourceIdentity from, IResourceIdentity to) 32 | { 33 | if (from == null) 34 | { 35 | throw new ArgumentNullException(nameof(from)); 36 | } 37 | 38 | if (to == null) 39 | { 40 | throw new ArgumentNullException(nameof(to)); 41 | } 42 | 43 | if (_comparer.Equals(from, to)) 44 | { 45 | throw new InvalidOperationException("Reflexive dependencies are not allowed."); 46 | } 47 | 48 | if (Edges.Any(e => _comparer.Equals(e.From, to) && _comparer.Equals(e.To, from))) 49 | { 50 | throw new InvalidOperationException("Unidirectional dependencies are not allowed."); 51 | } 52 | 53 | if (!Nodes.Contains(from)) 54 | { 55 | Nodes.Add(from); 56 | } 57 | 58 | if (!Nodes.Contains(to)) 59 | { 60 | Nodes.Add(to); 61 | } 62 | 63 | if (!Edges.Any(e => _comparer.Equals(e.From, from) && _comparer.Equals(e.To, to))) 64 | { 65 | Edges.Add(new ResourceGraphEdge(from, to)); 66 | } 67 | } 68 | 69 | public IEnumerable Traverse() 70 | { 71 | var walker = new ResourceGraphWalker(); 72 | return walker.Walk(this); 73 | } 74 | 75 | public ResourceGraph ShallowClone() 76 | { 77 | var graph = new ResourceGraph(); 78 | foreach (var edge in Edges) 79 | { 80 | graph.Connect(edge.From, edge.To); 81 | } 82 | 83 | foreach (var resource in Resources) 84 | { 85 | graph.Add(resource); 86 | } 87 | 88 | return graph; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Cupboard.Tests/Unit/Providers/FileProviderTests.cs: -------------------------------------------------------------------------------- 1 | using Cupboard.Testing; 2 | using Shouldly; 3 | using Spectre.IO; 4 | using Xunit; 5 | 6 | namespace Cupboard.Tests.Unit.Providers 7 | { 8 | public sealed class FileProviderTests 9 | { 10 | public sealed class Manifests 11 | { 12 | public sealed class FilePresent : Manifest 13 | { 14 | public override void Execute(ManifestContext context) 15 | { 16 | context.Resource("~/foo.txt") 17 | .Ensure(FileState.Present) 18 | .Source("~/bar/qux.txt"); 19 | } 20 | } 21 | 22 | public sealed class FileAbsent : Manifest 23 | { 24 | public override void Execute(ManifestContext context) 25 | { 26 | context.Resource("~/foo.txt") 27 | .Ensure(FileState.Absent); 28 | } 29 | } 30 | 31 | public sealed class SymlinkedFile : Manifest 32 | { 33 | public override void Execute(ManifestContext context) 34 | { 35 | context.Resource("~/foo.txt") 36 | .SymbolicLink() 37 | .Ensure(FileState.Present) 38 | .Source("~/bar/qux.txt"); 39 | } 40 | } 41 | } 42 | 43 | [Fact] 44 | public void Should_Copy_File() 45 | { 46 | // Given 47 | var fixture = new CupboardFixture(PlatformFamily.Linux); 48 | fixture.FileSystem.CreateFile("~/bar/qux.txt"); 49 | fixture.Configure(ctx => ctx.UseManifest()); 50 | 51 | // When 52 | var result = fixture.Run("-y"); 53 | 54 | // Then 55 | result.Report.GetState("~/foo.txt").ShouldBe(ResourceState.Changed); 56 | fixture.FileShouldExist("/home/Patrik/foo.txt"); 57 | } 58 | 59 | [Fact] 60 | public void Should_Create_Symlink() 61 | { 62 | // Given 63 | var fixture = new CupboardFixture(PlatformFamily.Linux); 64 | fixture.FileSystem.CreateFile("~/bar/qux.txt"); 65 | fixture.Configure(ctx => ctx.UseManifest()); 66 | 67 | // When 68 | var result = fixture.Run("-y"); 69 | 70 | // Then 71 | result.Report.GetState("~/foo.txt").ShouldBe(ResourceState.Changed); 72 | fixture.FileShouldExist("/home/Patrik/foo.txt"); 73 | fixture.FileShouldBeSymbolicLink("/home/Patrik/foo.txt"); 74 | } 75 | 76 | [Fact] 77 | public void Should_Delete_File() 78 | { 79 | // Given 80 | var fixture = new CupboardFixture(PlatformFamily.Linux); 81 | fixture.FileSystem.CreateFile("~/foo.txt"); 82 | fixture.Configure(ctx => ctx.UseManifest()); 83 | 84 | // When 85 | var result = fixture.Run("-y"); 86 | 87 | // Then 88 | result.Report.GetState("~/foo.txt").ShouldBe(ResourceState.Changed); 89 | fixture.FileShouldNotExist("/home/Patrik/foo.txt"); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@spectresystems.se. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/Cupboard.Core/Fact.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cupboard 5 | { 6 | public sealed class Fact 7 | { 8 | private static Fact DefaultFact { get; } = new Fact(null, string.Empty); 9 | 10 | public string Name { get; } 11 | public string FullName { get; } 12 | public object? Value { get; internal set; } 13 | public Fact this[string key] => GetChild(key); 14 | 15 | internal Fact? Parent { get; } 16 | internal Dictionary Children { get; } 17 | 18 | internal Fact(Fact? parent, string id, object? value = null) 19 | { 20 | Parent = parent; 21 | Name = id ?? throw new ArgumentNullException(nameof(id)); 22 | Value = value; 23 | Children = new Dictionary(StringComparer.OrdinalIgnoreCase); 24 | FullName = GetFullName(); 25 | } 26 | 27 | public T? As(T? defaultValue = default) 28 | where T : notnull 29 | { 30 | if (Value is T result) 31 | { 32 | return result; 33 | } 34 | 35 | return defaultValue; 36 | } 37 | 38 | private string GetFullName() 39 | { 40 | var stack = new Stack(); 41 | var current = this; 42 | while (current != null) 43 | { 44 | if (!string.IsNullOrWhiteSpace(current.Name)) 45 | { 46 | stack.Push(current.Name); 47 | } 48 | 49 | current = current.Parent; 50 | } 51 | 52 | return string.Join(".", stack); 53 | } 54 | 55 | private Fact GetChild(string key) 56 | { 57 | var parts = key.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); 58 | if (parts.Length > 1) 59 | { 60 | // Get full path 61 | var queue = new Queue(key.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries)); 62 | var root = this; 63 | while (queue.Count > 0) 64 | { 65 | var current = queue.Dequeue(); 66 | if (root.Children.TryGetValue(current, out var node)) 67 | { 68 | root = node; 69 | } 70 | else 71 | { 72 | return DefaultFact; 73 | } 74 | } 75 | 76 | return root; 77 | } 78 | else 79 | { 80 | Children.TryGetValue(key, out var fact); 81 | return fact ?? DefaultFact; 82 | } 83 | } 84 | 85 | public static implicit operator string(Fact fact) 86 | { 87 | return fact.Value as string ?? string.Empty; 88 | } 89 | 90 | public static implicit operator int(Fact fact) 91 | { 92 | return fact.Value is int value ? value : 0; 93 | } 94 | 95 | public static implicit operator bool(Fact fact) 96 | { 97 | return fact.Value is bool value && value; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Cupboard/ExecutionPlanBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Spectre.Console.Cli; 5 | 6 | namespace Cupboard.Internal 7 | { 8 | internal sealed class ExecutionPlanBuilder 9 | { 10 | private readonly ResourceProviderRepository _providers; 11 | 12 | public ExecutionPlanBuilder(ResourceProviderRepository providers) 13 | { 14 | _providers = providers ?? throw new ArgumentNullException(nameof(providers)); 15 | } 16 | 17 | public ExecutionPlan Build( 18 | IEnumerable catalogs, 19 | IEnumerable manifests, 20 | IFactBuilder factBuilder, 21 | IRemainingArguments args) 22 | { 23 | // Build facts 24 | var facts = factBuilder.Build(args); 25 | 26 | // Get all manifests added by catalogs 27 | var catalogManifests = GetCatalogManifests(catalogs, manifests, facts); 28 | 29 | // Build the resource graph 30 | var graph = ResourceGraphBuilder.Build(_providers, catalogManifests, facts); 31 | graph.Configurations.ForEach(action => action()); 32 | 33 | // Build the execution plan 34 | return Build(graph, facts); 35 | } 36 | 37 | private static IEnumerable GetCatalogManifests( 38 | IEnumerable catalogs, 39 | IEnumerable manifests, 40 | FactCollection facts) 41 | { 42 | // Execute all catalogs 43 | var ctx = new CatalogContext(facts); 44 | foreach (var catalog in catalogs) 45 | { 46 | if (catalog.CanRun(facts)) 47 | { 48 | catalog.Execute(ctx); 49 | } 50 | } 51 | 52 | var catalogManifests = ctx.GetManifests(); 53 | return manifests.Where(x => catalogManifests.Contains(x.GetType())); 54 | } 55 | 56 | private ExecutionPlan Build(ResourceGraph graph, FactCollection facts) 57 | { 58 | var resources = new List(); 59 | 60 | foreach (var node in graph.Traverse()) 61 | { 62 | // Find the resource. 63 | var resource = graph.Resources.SingleOrDefault(x => x.Name == node.Name); 64 | if (resource == null) 65 | { 66 | throw new InvalidOperationException($"Could not find resource '{node.Name}' ({node.ResourceType.Name})."); 67 | } 68 | 69 | // Get the provider. 70 | var provider = _providers.GetProvider(resource.ResourceType); 71 | if (provider == null) 72 | { 73 | throw new InvalidOperationException($"Could not find resource provider for '{node.Name}' ({node.ResourceType.Name})."); 74 | } 75 | 76 | // Do we need to be administrator for this resource? 77 | var requireAdministrator = resource.RequireAdministrator || provider.RequireAdministrator(facts); 78 | 79 | resources.Add(new ExecutionPlanItem(provider, resource, requireAdministrator)); 80 | } 81 | 82 | // Do we need to be administrator for this plan? 83 | var requiresAdministrator = resources.Any(item => item.RequireAdministrator); 84 | 85 | return new ExecutionPlan(resources, requiresAdministrator); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Cupboard/RebootDetector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace Cupboard 7 | { 8 | internal sealed class RebootDetector : IRebootDetector 9 | { 10 | private readonly IWindowsRegistry _registry; 11 | 12 | private readonly List _checkForExistence = new() 13 | { 14 | @"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending", 15 | @"HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress", 16 | @"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired", 17 | @"HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending", 18 | @"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting", 19 | @"HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttemps", 20 | }; 21 | 22 | private readonly List<(RegistryPath Path, string Value)> _checkForValue = new() 23 | { 24 | (@"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce", "DVDRebootSignal"), 25 | (@"HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon", "JoinDomain"), 26 | (@"HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon", "AvoidSpnSet"), 27 | (@"HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager", "PendingFileRenameOperations"), 28 | (@"HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager", "PendingFileRenameOperations2"), 29 | }; 30 | 31 | public RebootDetector(IWindowsRegistry registry) 32 | { 33 | _registry = registry ?? throw new ArgumentNullException(nameof(registry)); 34 | } 35 | 36 | public bool HasPendingReboot() 37 | { 38 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 39 | { 40 | return false; 41 | } 42 | 43 | // Check registry keys that must exist 44 | if (_checkForExistence.Any(path => KeyExist(path))) 45 | { 46 | return true; 47 | } 48 | 49 | // Check registry values that must exist 50 | if (_checkForValue.Any(p => KeyHasValue(p.Path, p.Value))) 51 | { 52 | return true; 53 | } 54 | 55 | // Check if Windows Update is waiting on updating any executables 56 | var wu = _registry.GetKey(@"HKLM:\SOFTWARE\Microsoft\Updates", writable: false); 57 | if (wu?.GetValue("UpdateExeVolatile") is int updateExeVolatile && updateExeVolatile != 0) 58 | { 59 | return true; 60 | } 61 | 62 | // Check for pending Windows updates 63 | wu = _registry.GetKey(@"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Services\Pending", writable: false); 64 | return wu?.GetValueCount() > 0; 65 | } 66 | 67 | private bool KeyExist(RegistryPath path) 68 | { 69 | var key = _registry.GetKey(path, writable: false); 70 | return key != null; 71 | } 72 | 73 | private bool KeyHasValue(RegistryPath path, string value) 74 | { 75 | var key = _registry.GetKey(path, writable: false); 76 | if (key != null) 77 | { 78 | return key.ValueExists(value); 79 | } 80 | 81 | return false; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | [*.cs] 4 | # IDE0055: Fix formatting 5 | dotnet_diagnostic.IDE0055.severity = warning 6 | 7 | # SA1101: Prefix local calls with this 8 | dotnet_diagnostic.SA1101.severity = none 9 | 10 | # SA1633: File should have header 11 | dotnet_diagnostic.SA1633.severity = none 12 | 13 | # SA1201: Elements should appear in the correct order 14 | dotnet_diagnostic.SA1201.severity = none 15 | 16 | # SA1202: Public members should come before private members 17 | dotnet_diagnostic.SA1202.severity = none 18 | 19 | # SA1309: Field names should not begin with underscore 20 | dotnet_diagnostic.SA1309.severity = none 21 | 22 | # SA1404: Code analysis suppressions should have justification 23 | dotnet_diagnostic.SA1404.severity = none 24 | 25 | # SA1516: Elements should be separated by a blank line 26 | dotnet_diagnostic.SA1516.severity = none 27 | 28 | # CA1303: Do not pass literals as localized parameters 29 | dotnet_diagnostic.CA1303.severity = none 30 | 31 | # CSA1204: Static members should appear before non-static members 32 | dotnet_diagnostic.SA1204.severity = none 33 | 34 | # IDE0052: Remove unread private members 35 | dotnet_diagnostic.IDE0052.severity = warning 36 | 37 | # IDE0063: Use simple 'using' statement 38 | csharp_prefer_simple_using_statement = false:suggestion 39 | 40 | # IDE0018: Variable declaration can be inlined 41 | dotnet_diagnostic.IDE0018.severity = warning 42 | 43 | # SA1625: Element documenation should not be copied and pasted 44 | dotnet_diagnostic.SA1625.severity = none 45 | 46 | # IDE0005: Using directive is unnecessary 47 | dotnet_diagnostic.IDE0005.severity = warning 48 | 49 | # SA1117: Parameters should be on same line or separate lines 50 | dotnet_diagnostic.SA1117.severity = none 51 | 52 | # SA1404: Code analysis suppression should have justification 53 | dotnet_diagnostic.SA1404.severity = none 54 | 55 | # SA1101: Prefix local calls with this 56 | dotnet_diagnostic.SA1101.severity = none 57 | 58 | # SA1633: File should have header 59 | dotnet_diagnostic.SA1633.severity = none 60 | 61 | # SA1649: File name should match first type name 62 | dotnet_diagnostic.SA1649.severity = none 63 | 64 | # SA1402: File may only contain a single type 65 | dotnet_diagnostic.SA1402.severity = none 66 | 67 | # CA1814: Prefer jagged arrays over multidimensional 68 | dotnet_diagnostic.CA1814.severity = none 69 | 70 | # RCS1194: Implement exception constructors. 71 | dotnet_diagnostic.RCS1194.severity = none 72 | 73 | # CA1032: Implement standard exception constructors 74 | dotnet_diagnostic.CA1032.severity = none 75 | 76 | # CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly 77 | dotnet_diagnostic.CA1826.severity = none 78 | 79 | # RCS1079: Throwing of new NotImplementedException. 80 | dotnet_diagnostic.RCS1079.severity = warning 81 | 82 | # RCS1057: Add empty line between declarations. 83 | dotnet_diagnostic.RCS1057.severity = none 84 | 85 | # IDE0004: Remove Unnecessary Cast 86 | dotnet_diagnostic.IDE0004.severity = warning 87 | 88 | # CA1034: Nested types should not be visible 89 | dotnet_diagnostic.CA1034.severity = none 90 | 91 | # CS1591: Missing XML comment for publicly visible type or member 92 | dotnet_diagnostic.CS1591.severity = none 93 | 94 | # SA1600: Elements should be documented 95 | dotnet_diagnostic.SA1600.severity = none 96 | 97 | # SA1601: Partial elements should be documented 98 | dotnet_diagnostic.SA1601.severity = none 99 | 100 | # SA1600: Enumeration items should be documented 101 | dotnet_diagnostic.SA1602.severity = none 102 | 103 | # RCS1036: Remove redundant empty line. 104 | dotnet_diagnostic.RCS1036.severity = warning -------------------------------------------------------------------------------- /src/Cupboard.Providers/MachineFacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using Spectre.Console.Cli; 5 | using Platform = System.Runtime.InteropServices.OSPlatform; 6 | 7 | namespace Cupboard 8 | { 9 | internal sealed class MachineFacts : IFactProvider 10 | { 11 | IEnumerable<(string Name, object Value)> IFactProvider.GetFacts(IRemainingArguments args) 12 | { 13 | yield return ("os.arch", RuntimeInformation.OSArchitecture); 14 | yield return ("os.platform", GetOSPlatform()); 15 | yield return ("machine.name", Environment.MachineName); 16 | yield return ("computer.name", Environment.MachineName); 17 | yield return ("user.name", Environment.UserName); 18 | } 19 | 20 | private static OSPlatform GetOSPlatform() 21 | { 22 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 23 | { 24 | return OSPlatform.Windows; 25 | } 26 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 27 | { 28 | return OSPlatform.OSX; 29 | } 30 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 31 | { 32 | return OSPlatform.Linux; 33 | } 34 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) 35 | { 36 | return OSPlatform.FreeBSD; 37 | } 38 | else 39 | { 40 | throw new InvalidOperationException("Unknown OS platform"); 41 | } 42 | } 43 | } 44 | 45 | public static partial class MachineFactsExtensions 46 | { 47 | public static Platform OSPlatform(this FactCollection facts) 48 | { 49 | return facts["os"]["platform"].As(); 50 | } 51 | 52 | public static Architecture OSArchitecture(this FactCollection facts) 53 | { 54 | return facts["os"]["arch"].As(); 55 | } 56 | 57 | public static bool IsWindows(this FactCollection facts) 58 | { 59 | return OSPlatform(facts) == Platform.Windows; 60 | } 61 | 62 | public static bool IsLinux(this FactCollection facts) 63 | { 64 | return OSPlatform(facts) == Platform.Linux; 65 | } 66 | 67 | public static bool IsMacOS(this FactCollection facts) 68 | { 69 | return OSPlatform(facts) == Platform.OSX; 70 | } 71 | 72 | public static bool IsFreeBSD(this FactCollection facts) 73 | { 74 | return OSPlatform(facts) == Platform.FreeBSD; 75 | } 76 | 77 | public static bool IsX86(this FactCollection facts) 78 | { 79 | return OSArchitecture(facts) is Architecture.X86; 80 | } 81 | 82 | public static bool IsX64(this FactCollection facts) 83 | { 84 | return OSArchitecture(facts) is Architecture.X64; 85 | } 86 | 87 | public static bool IsX86OrX64(this FactCollection facts) 88 | { 89 | return IsX86(facts) || IsX64(facts); 90 | } 91 | 92 | public static bool IsArm(this FactCollection facts) 93 | { 94 | return OSArchitecture(facts) is Architecture.Arm or Architecture.Arm64; 95 | } 96 | 97 | public static bool IsArm64(this FactCollection facts) 98 | { 99 | return OSArchitecture(facts) is Architecture.Arm64; 100 | } 101 | 102 | public static bool IsArmOrArm64(this FactCollection facts) 103 | { 104 | return IsX86(facts) || IsX64(facts); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/Chmod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Cupboard.Internal; 4 | using Mono.Unix; 5 | 6 | namespace Cupboard 7 | { 8 | public sealed class Chmod 9 | { 10 | private static readonly Dictionary, Dictionary> _lookup; 11 | 12 | public SpecialMode Mode { get; } 13 | public Permissions Owner { get; } 14 | public Permissions Group { get; } 15 | public Permissions Other { get; } 16 | 17 | static Chmod() 18 | { 19 | _lookup = new Dictionary, Dictionary> 20 | { 21 | [c => c.Owner] = new Dictionary 22 | { 23 | { Permissions.Read, FileAccessPermissions.UserRead }, 24 | { Permissions.Write, FileAccessPermissions.UserWrite }, 25 | { Permissions.Execute, FileAccessPermissions.UserExecute }, 26 | }, 27 | [c => c.Group] = new Dictionary 28 | { 29 | { Permissions.Read, FileAccessPermissions.GroupRead }, 30 | { Permissions.Write, FileAccessPermissions.GroupWrite }, 31 | { Permissions.Execute, FileAccessPermissions.GroupExecute }, 32 | }, 33 | [c => c.Other] = new Dictionary 34 | { 35 | { Permissions.Read, FileAccessPermissions.OtherRead }, 36 | { Permissions.Write, FileAccessPermissions.OtherWrite }, 37 | { Permissions.Execute, FileAccessPermissions.OtherExecute }, 38 | }, 39 | }; 40 | } 41 | 42 | public Chmod(Permissions owner, Permissions group, Permissions other) 43 | : this(SpecialMode.None, owner, group, other) 44 | { 45 | } 46 | 47 | public Chmod(SpecialMode mode, Permissions owner, Permissions group, Permissions other) 48 | { 49 | Mode = mode; 50 | Owner = owner; 51 | Group = group; 52 | Other = other; 53 | } 54 | 55 | public static Chmod Parse(string pattern) 56 | { 57 | return ChmodParser.Parse(pattern); 58 | } 59 | 60 | public Permissions GetPermissions(ChmodClass @class) 61 | { 62 | return @class switch 63 | { 64 | ChmodClass.Owner => Owner, 65 | ChmodClass.Group => Group, 66 | ChmodClass.Other => Other, 67 | _ => throw new NotSupportedException("Invalid chmod reference."), 68 | }; 69 | } 70 | 71 | public FileAccessPermissions ToFileAccessPermissions() 72 | { 73 | var permissions = default(FileAccessPermissions); 74 | foreach (var map in _lookup) 75 | { 76 | var permission = map.Key(this); 77 | foreach (var p in map.Value) 78 | { 79 | if (permission.HasFlag(p.Key)) 80 | { 81 | permissions |= p.Value; 82 | } 83 | } 84 | } 85 | 86 | return permissions; 87 | } 88 | 89 | public string ToString(ChmodFormatting formatting) 90 | { 91 | return ChmodFormatter.Format(this, formatting); 92 | } 93 | 94 | public override string ToString() 95 | { 96 | return ChmodFormatter.Format(this, ChmodFormatting.Numeric); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Cupboard/CupboardHostBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Cupboard.Internal; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Spectre.Console; 7 | using Spectre.Console.Cli; 8 | using Spectre.IO; 9 | 10 | namespace Cupboard 11 | { 12 | public sealed class CupboardHostBuilder 13 | { 14 | private readonly List> _configurations; 15 | private readonly IAnsiConsole _console; 16 | private bool _propagateExceptions = false; 17 | 18 | public CupboardHostBuilder(IAnsiConsole? console = null) 19 | { 20 | _console = console ?? AnsiConsole.Console; 21 | _configurations = new List> 22 | { 23 | builder => builder.ConfigureServices(services => 24 | { 25 | services.AddAll(); 26 | 27 | services.AddModule(); 28 | services.AddModule(); 29 | 30 | services.AddSingleton(_console); 31 | services.AddSingleton(); 32 | services.AddSingleton(); 33 | services.AddSingleton(); 34 | 35 | services.AddSingleton(); 36 | 37 | services.AddSingleton(); 38 | services.AddSingleton(); 39 | services.AddSingleton(); 40 | services.AddSingleton(); 41 | services.AddSingleton(); 42 | 43 | services.AddSingleton(); 44 | services.AddSingleton(); 45 | services.AddSingleton(); 46 | services.AddSingleton(); 47 | services.AddSingleton(); 48 | services.AddSingleton(); 49 | 50 | services.AddCommandLine(config => 51 | { 52 | config.ConfigureConsole(_console); 53 | config.PropagateExceptions(); 54 | 55 | config.AddCommand("facts"); 56 | }); 57 | }), 58 | }; 59 | } 60 | 61 | public CupboardHostBuilder PropagateExceptions() 62 | { 63 | _propagateExceptions = true; 64 | return this; 65 | } 66 | 67 | public CupboardHostBuilder AddCatalog() 68 | where TCatalog : Catalog 69 | { 70 | _configurations.Add(b => b.ConfigureServices(s => s.AddSingleton())); 71 | return this; 72 | } 73 | 74 | public CupboardHostBuilder ConfigureServices(Action services) 75 | { 76 | _configurations.Add(b => b.ConfigureServices(services)); 77 | return this; 78 | } 79 | 80 | public CupboardHost Build() 81 | { 82 | var builder = Host.CreateDefaultBuilder(); 83 | _configurations.ForEach(action => action(builder)); 84 | 85 | var host = builder.Build(); 86 | var console = host.Services.GetRequiredService(); 87 | 88 | return new CupboardHost(console, host, _propagateExceptions); 89 | } 90 | 91 | public int Run(string[] args) 92 | { 93 | var host = Build(); 94 | return host.Run(args); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Cupboard/ResourceGraphWalker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Cupboard.Internal 6 | { 7 | internal sealed class ResourceGraphWalker 8 | { 9 | private readonly ResourceComparer _comparer; 10 | 11 | public ResourceGraphWalker() 12 | { 13 | _comparer = new ResourceComparer(); 14 | } 15 | 16 | private class TargetResource : Resource 17 | { 18 | public TargetResource() 19 | : base("__target__") 20 | { 21 | } 22 | } 23 | 24 | public IEnumerable Walk(ResourceGraph graph) 25 | { 26 | // Clone the graph. 27 | graph = graph.ShallowClone(); 28 | 29 | // Sanity check to make sure that all edges exist in the graph. 30 | foreach (var edge in graph.Edges) 31 | { 32 | EnsureResourceExist(graph, edge.From); 33 | EnsureResourceExist(graph, edge.To); 34 | } 35 | 36 | // Find all nodes without any edges. 37 | var orphans = graph.Resources 38 | .Where(x => !graph.Edges.Any(edge => _comparer.Equals(x, edge.From)) 39 | && !graph.Edges.Any(edge => _comparer.Equals(x, edge.To))) 40 | .ToArray(); 41 | 42 | // Find all leaves in the graph. 43 | var leaves = graph.Nodes 44 | .Where(x => graph.Edges.Any(y => _comparer.Equals(x, y.To)) 45 | && !graph.Edges.Any(z => _comparer.Equals(x, z.From))) 46 | .ToArray(); 47 | 48 | // Add an artifical destination node to all leaves. 49 | // This will be the target to traverse to it's roots. 50 | var target = new TargetResource(); 51 | foreach (var leaf in leaves.Concat(orphans)) 52 | { 53 | graph.Connect(leaf, target); 54 | } 55 | 56 | // Traverse the graph. 57 | var result = new List(); 58 | Traverse(graph, target, result); 59 | 60 | // Remove the target node from the results. 61 | result.RemoveAll(x => x.ResourceType == typeof(TargetResource)); 62 | 63 | // Return the result. 64 | return result; 65 | } 66 | 67 | private void Traverse( 68 | ResourceGraph graph, 69 | IResourceIdentity node, 70 | ICollection result, 71 | ISet? visited = null) 72 | { 73 | visited ??= new HashSet(_comparer); 74 | if (!visited.Contains(node)) 75 | { 76 | visited.Add(node); 77 | var incoming = graph.Edges.Where(x => _comparer.Equals(x.To, node)).Select(x => x.From); 78 | foreach (var child in incoming) 79 | { 80 | Traverse(graph, child, result, visited); 81 | } 82 | 83 | result.Add(node); 84 | } 85 | else if (!result.Any(x => _comparer.Equals(x, node))) 86 | { 87 | throw new InvalidOperationException("Graph contains circular references."); 88 | } 89 | } 90 | 91 | private static void EnsureResourceExist(ResourceGraph graph, IResourceIdentity identity) 92 | { 93 | if (!graph.Resources.Any(r => r.Name.Equals(identity.Name, StringComparison.OrdinalIgnoreCase) 94 | && r.ResourceType == identity.ResourceType)) 95 | { 96 | throw new InvalidOperationException( 97 | $"Could not find resource '{identity.Name}' of type '{identity.ResourceType.Name}'."); 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Cupboard.Core/IO/RegistryPath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Cupboard 6 | { 7 | public sealed class RegistryPath 8 | { 9 | private static readonly Dictionary _rootSubstitutions; 10 | private static readonly Dictionary _roots; 11 | 12 | public RegistryHive Hive { get; set; } 13 | public string Path { get; set; } 14 | public string SubKey { get; } 15 | public IReadOnlyList Segments { get; } 16 | 17 | public bool IsValid { get; } 18 | 19 | static RegistryPath() 20 | { 21 | _rootSubstitutions = new Dictionary(StringComparer.OrdinalIgnoreCase) 22 | { 23 | { "HKCR", "HKEY_CLASSES_ROOT" }, 24 | { "HKCU", "HKEY_CURRENT_USER" }, 25 | { "HKLM", "HKEY_LOCAL_MACHINE" }, 26 | { "HKU", "HKEY_USERS" }, 27 | { "HKPD", "HKEY_PERFORMANCE_DATA" }, 28 | { "HKCC", "HKEY_CURRENT_CONFIG" }, 29 | { "HKCR:", "HKEY_CLASSES_ROOT" }, 30 | { "HKCU:", "HKEY_CURRENT_USER" }, 31 | { "HKLM:", "HKEY_LOCAL_MACHINE" }, 32 | { "HKU:", "HKEY_USERS" }, 33 | { "HKPD:", "HKEY_PERFORMANCE_DATA" }, 34 | { "HKCC:", "HKEY_CURRENT_CONFIG" }, 35 | }; 36 | 37 | _roots = new Dictionary(StringComparer.OrdinalIgnoreCase) 38 | { 39 | { "HKEY_CLASSES_ROOT", RegistryHive.ClassesRoot }, 40 | { "HKEY_CURRENT_CONFIG", RegistryHive.CurrentConfig }, 41 | { "HKEY_CURRENT_USER", RegistryHive.CurrentUser }, 42 | { "HKEY_LOCAL_MACHINE", RegistryHive.LocalMachine }, 43 | { "HKEY_PERFORMANCE_DATA", RegistryHive.PerformanceData }, 44 | { "HKEY_USERS", RegistryHive.Users }, 45 | }; 46 | } 47 | 48 | public RegistryPath(string path) 49 | { 50 | path ??= string.Empty; 51 | var key = path.Replace("/", "\\"); 52 | 53 | Path = string.Empty; 54 | Segments = new List(key.Split('\\')); 55 | 56 | if (Segments.Count > 0) 57 | { 58 | Hive = GetRoot(Segments[0]); 59 | Segments = Segments.Skip(1).ToList(); 60 | Path = string.Join("\\", Segments); 61 | } 62 | 63 | SubKey = string.Join("\\", Segments.Take(Segments.Count)); 64 | 65 | IsValid = Segments.Count > 1 66 | && Hive != RegistryHive.Unknown 67 | && !string.IsNullOrWhiteSpace(Path); 68 | } 69 | 70 | private static RegistryHive GetRoot(string rootName) 71 | { 72 | if (_rootSubstitutions.ContainsKey(rootName)) 73 | { 74 | rootName = _rootSubstitutions[rootName]; 75 | } 76 | 77 | if (!_roots.TryGetValue(rootName, out var r)) 78 | { 79 | return RegistryHive.Unknown; 80 | } 81 | 82 | return r; 83 | } 84 | 85 | private string? GetRootName() 86 | { 87 | return Hive switch 88 | { 89 | RegistryHive.ClassesRoot => "HKEY_CLASSES_ROOT", 90 | RegistryHive.CurrentUser => "HKEY_CURRENT_USER", 91 | RegistryHive.LocalMachine => "HKEY_LOCAL_MACHINE", 92 | RegistryHive.Users => "HKEY_USERS", 93 | RegistryHive.CurrentConfig => "HKEY_CURRENT_CONFIG", 94 | _ => null, 95 | }; 96 | } 97 | 98 | public static implicit operator RegistryPath(string path) 99 | { 100 | return new RegistryPath(path); 101 | } 102 | 103 | public override string ToString() 104 | { 105 | return string.Concat(GetRootName(), "\\", Path); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Cupboard.Providers/Directory/DirectoryProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Spectre.IO; 4 | 5 | namespace Cupboard 6 | { 7 | public sealed class DirectoryProvider : ResourceProvider 8 | { 9 | private readonly ICupboardFileSystem _fileSystem; 10 | private readonly ICupboardEnvironment _environment; 11 | private readonly ICupboardLogger _logger; 12 | private readonly IDictionary> _map; 13 | 14 | public DirectoryProvider( 15 | ICupboardFileSystem fileSystem, 16 | ICupboardEnvironment environment, 17 | ICupboardLogger logger) 18 | { 19 | _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); 20 | _environment = environment ?? throw new ArgumentNullException(nameof(environment)); 21 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 22 | 23 | _map = new Dictionary> 24 | { 25 | { DirectoryState.Present, CreateDirectory }, 26 | { DirectoryState.Absent, DeleteDirectory }, 27 | }; 28 | } 29 | 30 | public override Directory Create(string name) 31 | { 32 | return new Directory(name); 33 | } 34 | 35 | public override ResourceState Run(IExecutionContext context, Directory resource) 36 | { 37 | if (_map.TryGetValue(resource.Ensure, out var action)) 38 | { 39 | return action.Invoke(context, resource); 40 | } 41 | 42 | return ResourceState.Unknown; 43 | } 44 | 45 | private ResourceState DeleteDirectory(IExecutionContext context, Directory resource) 46 | { 47 | if (resource.Path == null) 48 | { 49 | _logger.Error($"The resource '{resource.Name}' does not have a path"); 50 | return ResourceState.Error; 51 | } 52 | 53 | var path = resource.Path.MakeAbsolute(_environment); 54 | if (_fileSystem.Exist(path)) 55 | { 56 | if (!context.DryRun) 57 | { 58 | _logger.Information($"Deleting directory '{path.FullPath}'."); 59 | _fileSystem.Directory.Delete(path, true); 60 | } 61 | 62 | return ResourceState.Changed; 63 | } 64 | else 65 | { 66 | _logger.Information($"Directory '{path.FullPath}' does not exist."); 67 | return ResourceState.Unchanged; 68 | } 69 | } 70 | 71 | private ResourceState CreateDirectory(IExecutionContext context, Directory resource) 72 | { 73 | if (resource.Path == null) 74 | { 75 | _logger.Error($"The resource '{resource.Name}' does not have a path"); 76 | return ResourceState.Error; 77 | } 78 | 79 | var path = resource.Path.MakeAbsolute(_environment); 80 | 81 | if (!_fileSystem.Exist(path)) 82 | { 83 | if (!context.DryRun) 84 | { 85 | _logger.Information($"Creating directory '{path.FullPath}'."); 86 | _fileSystem.Directory.Create(path); 87 | 88 | // Not on Windows and got permissions set? 89 | if (resource.Permissions != null && _environment.Platform.Family != PlatformFamily.Windows) 90 | { 91 | path.SetPermissions(resource.Permissions); 92 | } 93 | } 94 | 95 | return ResourceState.Changed; 96 | } 97 | else 98 | { 99 | _logger.Information($"Directory '{path.FullPath}' already exists."); 100 | return ResourceState.Unchanged; 101 | } 102 | } 103 | } 104 | } 105 | --------------------------------------------------------------------------------