diff --git a/.editorconfig b/.editorconfig index 46a2f0f..776fb87 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,26 @@ insert_final_newline = true tab_width = 2 trim_trailing_whitespace = true +[*.cs] +csharp_indent_case_contents_when_block = false +csharp_indent_labels = no_change +csharp_new_line_before_open_brace = none +csharp_prefer_braces = false +csharp_space_after_cast = true +csharp_space_before_colon_in_inheritance_clause = false +csharp_style_expression_bodied_constructors = true +csharp_style_expression_bodied_local_functions = true +csharp_style_expression_bodied_methods = true +csharp_style_expression_bodied_operators = true +csharp_style_namespace_declarations = file_scoped +csharp_style_var_elsewhere = true +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true +csharp_using_directive_placement = inside_namespace +dotnet_diagnostic.CS8524.severity = silent +dotnet_sort_system_directives_first = false +dotnet_style_namespace_match_folder = false + [*.md] trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes index c510df3..c16a191 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,21 +1,30 @@ * text=auto .* text +*.cjs text *.config text *.cs text diff=csharp *.csproj text *.css text diff=css +*.esproj text *.html text diff=html *.http text *.ini text -*.iss text +*.js text *.json text *.md text diff=markdown +*.mjs text +*.php text diff=php *.ps1 text +*.psd1 text +*.razor text +*.scss text diff=css *.slnx text *.sql text *.svg text +*.ts text *.txt text +*.webmanifest text *.xml text *.yaml text *.yml text diff --git a/.gitignore b/.gitignore index b4ce5b1..03555ec 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ /.idea/ /.vs/ /bin/ -/src/obj/ +/*/obj/ /var/ diff --git a/.runsettings b/.runsettings new file mode 100644 index 0000000..5aaedf1 --- /dev/null +++ b/.runsettings @@ -0,0 +1,5 @@ + + + var/TestResults + + diff --git a/.vscode/extensions.json b/.vscode/extensions.json index b408c54..e2d0e78 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ + "ms-dotnettools.csdevkit", "ms-vscode.powershell" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 93c907c..ec38296 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,6 +3,7 @@ "configurations": [ { "name": "Script", + "preLaunchTask": "Build", "request": "launch", "type": "PowerShell", "script": "${workspaceFolder}/Debug.ps1" diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..b3a0bfc --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,11 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build", + "type": "shell", + "command": "./Invoke.ps1", + "args": ["Build"] + } + ] +} diff --git a/Invoke.ps1 b/Invoke.ps1 index e96d6ee..6be28b1 100755 --- a/Invoke.ps1 +++ b/Invoke.ps1 @@ -5,7 +5,10 @@ param ( param ($commandName, $parameterName, $wordToComplete) (Get-Item "$PSScriptRoot/tool/$wordToComplete*.ps1").BaseName })] - [string] $Command = "Default" + [string] $Command = "Default", + + [Parameter()] + [switch] $Release ) $ErrorActionPreference = "Stop" diff --git a/SetupHashLink.psd1 b/SetupHashLink.psd1 index e3a99be..4551ba7 100644 --- a/SetupHashLink.psd1 +++ b/SetupHashLink.psd1 @@ -1,8 +1,8 @@ @{ DefaultCommandPrefix = "HashLink" - ModuleVersion = "7.1.0" + ModuleVersion = "8.0.0" PowerShellVersion = "7.4" - RootModule = "src/Main.psm1" + RootModule = "bin/Belin.SetupHashLink.dll" Author = "Cédric Belin " CompanyName = "Cedric-Belin.fr" @@ -11,10 +11,10 @@ GUID = "6bb1e481-9f7c-4dd0-922c-fdf44f2c0e78" AliasesToExport = @() - CmdletsToExport = @() + FunctionsToExport = @() VariablesToExport = @() - FunctionsToExport = @( + CmdletsToExport = @( "Find-Release" "Get-Release" "Install-Release" @@ -23,12 +23,6 @@ "Test-Release" ) - NestedModules = @( - "src/Platform.psm1" - "src/Release.psm1" - "src/Setup.psm1" - ) - PrivateData = @{ PSData = @{ LicenseUri = "https://github.com/cedx/setup-hashlink/blob/main/License.md" diff --git a/SetupHashLink.slnx b/SetupHashLink.slnx index 5a8f7f4..28aa23e 100644 --- a/SetupHashLink.slnx +++ b/SetupHashLink.slnx @@ -3,6 +3,7 @@ + @@ -21,24 +22,25 @@ + + + + + - - - - - - - + + + diff --git a/Start.ps1 b/Start.ps1 index de15d30..ea94d3f 100755 --- a/Start.ps1 +++ b/Start.ps1 @@ -1,14 +1,14 @@ #!/usr/bin/env pwsh -using module ./src/Release.psm1 -using module ./src/Setup.psm1 +using namespace Belin.SetupHashLink +Add-Type -Path "$PSScriptRoot/bin/Belin.SetupHashLink.dll" $ErrorActionPreference = "Stop" $PSNativeCommandUseErrorActionPreference = $true Set-StrictMode -Version Latest -if (-not (Test-Path Env:SETUP_HASHLINK_VERSION)) { $Env:SETUP_HASHLINK_VERSION = "latest" } +if (-not (Test-Path Env:SETUP_HASHLINK_VERSION)) { $Env:SETUP_HASHLINK_VERSION = "Latest" } $release = [Release]::Find($Env:SETUP_HASHLINK_VERSION) -if (-not $release) { throw "No release matching the version constraint." } +if (-not $release) { throw "No release matches the specified version constraint." } -$path = [Setup]::new($release).Install() +$path = [Setup]::new($release).Install().GetAwaiter().GetResult() "HashLink $($release.Version) successfully installed in ""$path""." diff --git a/example/workflow.yaml b/example/workflow.yaml index 0dc3a8b..a449ab2 100644 --- a/example/workflow.yaml +++ b/example/workflow.yaml @@ -17,7 +17,7 @@ jobs: - name: Set up Lix uses: lix-pm/setup-lix@master - name: Set up HashLink - uses: cedx/setup-hashlink@v7 + uses: cedx/setup-hashlink@v8 with: version: latest - name: Install dependencies diff --git a/src/Cmdlets/Find-Release.cs b/src/Cmdlets/Find-Release.cs new file mode 100644 index 0000000..b9f3d10 --- /dev/null +++ b/src/Cmdlets/Find-Release.cs @@ -0,0 +1,20 @@ +namespace Belin.SetupHashLink.Cmdlets; + +/// +/// Finds a release that matches the specified version constraint. +/// +[Cmdlet(VerbsCommon.Find, "Release")] +[OutputType(typeof(Release))] +public class FindReleaseCommand: Cmdlet { + + /// + /// The version constraint. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)] + public required string Constraint { get; set; } + + /// + /// Performs execution of this command. + /// + protected override void ProcessRecord() => WriteObject(Release.Find(Constraint)); +} diff --git a/src/Cmdlets/Get-Platform.cs b/src/Cmdlets/Get-Platform.cs new file mode 100644 index 0000000..edd681e --- /dev/null +++ b/src/Cmdlets/Get-Platform.cs @@ -0,0 +1,14 @@ +namespace Belin.SetupHashLink.Cmdlets; + +/// +/// Gets the current platform. +/// +[Cmdlet(VerbsCommon.Get, "Platform")] +[OutputType(typeof(Platform))] +public class GetPlatformCommand: Cmdlet { + + /// + /// Performs execution of this command. + /// + protected override void ProcessRecord() => WriteObject(PlatformExtensions.GetCurrent()); +} diff --git a/src/Cmdlets/Get-Release.cs b/src/Cmdlets/Get-Release.cs new file mode 100644 index 0000000..01c17f4 --- /dev/null +++ b/src/Cmdlets/Get-Release.cs @@ -0,0 +1,21 @@ +namespace Belin.SetupHashLink.Cmdlets; + +/// +/// Gets the release corresponding to the specified version. +/// +[Cmdlet(VerbsCommon.Get, "Release")] +[OutputType(typeof(Release))] +public class GetReleaseCommand: Cmdlet { + + /// + /// The version number. Use `*` or `Latest` to get the latest release. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)] + public required string Version { get; set; } + + /// + /// Performs execution of this command. + /// + protected override void ProcessRecord() => + WriteObject(Release.LatestReleasePattern().IsMatch(Version) ? Release.Latest : Release.Get(Version)); +} diff --git a/src/Cmdlets/Install-Release.cs b/src/Cmdlets/Install-Release.cs new file mode 100644 index 0000000..f76f613 --- /dev/null +++ b/src/Cmdlets/Install-Release.cs @@ -0,0 +1,33 @@ +namespace Belin.SetupHashLink.Cmdlets; + +/// +/// Installs the HashLink VM, after downloading it. +/// +[Cmdlet(VerbsLifecycle.Install, "Release", DefaultParameterSetName = "Constraint")] +[OutputType(typeof(string))] +public class InstallReleaseCommand: PSCmdlet { + + /// + /// The version constraint of the release to be installed. + /// + [Parameter(Mandatory = true, ParameterSetName = "Constraint", Position = 0, ValueFromPipeline = true)] + public required string Constraint { get; set; } + + /// + /// The release to be installed. + /// + [Parameter(Mandatory = true, ParameterSetName = "InputObject", ValueFromPipeline = true)] + public required Release InputObject { get; set; } + + /// + /// Performs execution of this command. + /// + protected override void ProcessRecord() { + var release = ParameterSetName == "InputObject" ? InputObject : Release.Find(Constraint); + if (release?.Exists ?? false) WriteObject(new Setup(release).Install().GetAwaiter().GetResult()); + else { + var exception = new InvalidOperationException("No release matches the specified version constraint."); + WriteError(new ErrorRecord(exception, "ReleaseNotFound", ErrorCategory.ObjectNotFound, null)); + } + } +} diff --git a/src/Cmdlets/New-Release.cs b/src/Cmdlets/New-Release.cs new file mode 100644 index 0000000..b032137 --- /dev/null +++ b/src/Cmdlets/New-Release.cs @@ -0,0 +1,26 @@ +namespace Belin.SetupHashLink.Cmdlets; + +/// +/// Creates a new release. +/// +[Cmdlet(VerbsCommon.New, "Release")] +[OutputType(typeof(Release))] +public class NewReleaseCommand: Cmdlet { + + /// + /// The associated assets. + /// + [Parameter(Position = 1)] + public Release.Asset[] Assets { get; set; } = []; + + /// + /// The version number. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)] + public required Version Version { get; set; } + + /// + /// Performs execution of this command. + /// + protected override void ProcessRecord() => WriteObject(new Release(Version, Assets)); +} diff --git a/src/Cmdlets/New-ReleaseAsset.cs b/src/Cmdlets/New-ReleaseAsset.cs new file mode 100644 index 0000000..91602e2 --- /dev/null +++ b/src/Cmdlets/New-ReleaseAsset.cs @@ -0,0 +1,26 @@ +namespace Belin.SetupHashLink.Cmdlets; + +/// +/// Creates a new release asset. +/// +[Cmdlet(VerbsCommon.New, "ReleaseAsset")] +[OutputType(typeof(Release.Asset))] +public class NewReleaseAssetCommand: Cmdlet { + + /// + /// The target file. + /// + [Parameter(Mandatory = true, Position = 1)] + public required string File { get; set; } + + /// + /// The target platform. + /// + [Parameter(Mandatory = true, Position = 0)] + public required Platform Platform { get; set; } + + /// + /// Performs execution of this command. + /// + protected override void ProcessRecord() => WriteObject(new Release.Asset(Platform, File)); +} diff --git a/src/Cmdlets/Test-Release.cs b/src/Cmdlets/Test-Release.cs new file mode 100644 index 0000000..e512593 --- /dev/null +++ b/src/Cmdlets/Test-Release.cs @@ -0,0 +1,29 @@ +namespace Belin.SetupHashLink.Cmdlets; + +/// +/// Gets a value indicating whether a release with the specified version exists. +/// +[Cmdlet(VerbsDiagnostic.Test, "Release", DefaultParameterSetName = "Version")] +[OutputType(typeof(bool))] +public class TestReleaseCommand: PSCmdlet { + + /// + /// The release to be tested. + /// + [Parameter(Mandatory = true, ParameterSetName = "InputObject", ValueFromPipeline = true)] + public required Release InputObject { get; set; } + + /// + /// The version number of the release to be tested. + /// + [Parameter(Mandatory = true, ParameterSetName = "Version", Position = 0, ValueFromPipeline = true)] + public required Version Version { get; set; } + + /// + /// Performs execution of this command. + /// + protected override void ProcessRecord() { + var release = ParameterSetName == "InputObject" ? InputObject : new Release(Version); + WriteObject(release.Exists); + } +} diff --git a/src/Data.psd1 b/src/Data.psd1 deleted file mode 100644 index 152b252..0000000 --- a/src/Data.psd1 +++ /dev/null @@ -1,20 +0,0 @@ -@{ - Releases = @( - @{ Version = "1.15.0"; Assets = , @{ Platform = "Windows"; File = "hashlink-1.15.0-win.zip" } } - @{ Version = "1.14.0"; Assets = , @{ Platform = "Windows"; File = "hashlink-1.14.0-win.zip" } } - @{ Version = "1.13.0"; Assets = , @{ Platform = "Windows"; File = "hashlink-1.13.0-win.zip" } } - @{ Version = "1.12.0"; Assets = , @{ Platform = "Windows"; File = "hl-1.12.0-win.zip" } } - @{ Version = "1.11.0"; Assets = , @{ Platform = "Windows"; File = "hl-1.11.0-win.zip" } } - @{ Version = "1.10.0"; Assets = , @{ Platform = "Windows"; File = "hl-1.10.0-win.zip" } } - @{ Version = "1.9.0"; Assets = , @{ Platform = "Windows"; File = "hl-1.9.0-win.zip" } } - @{ Version = "1.8.0"; Assets = , @{ Platform = "Windows"; File = "hl-1.8.0-win.zip" } } - @{ Version = "1.7.0"; Assets = , @{ Platform = "Windows"; File = "hl-1.7.0-win.zip" } } - @{ Version = "1.6.0"; Assets = @{ Platform = "Windows"; File = "hl-1.6.0-win.zip" }, @{ Platform = "Linux"; File = "hl-1.6.0-linux.tgz" } } - @{ Version = "1.5.0"; Assets = @{ Platform = "Windows"; File = "hl-1.5.0-win.zip" }, @{ Platform = "Linux"; File = "hl-1.5.0-linux.tgz" } } - @{ Version = "1.4.0"; Assets = , @{ Platform = "Windows"; File = "hl-1.4-win.zip" } } - @{ Version = "1.3.0"; Assets = @{ Platform = "Windows"; File = "hl-1.3-win32.zip" }, @{ Platform = "MacOS"; File = "hl-1.3-osx32.zip" } } - @{ Version = "1.2.0"; Assets = @{ Platform = "Windows"; File = "hl-1.2-win32.zip" }, @{ Platform = "MacOS"; File = "hl-1.2-osx.zip" } } - @{ Version = "1.1.0"; Assets = , @{ Platform = "Windows"; File = "hl-1.1-win32.zip" } } - @{ Version = "1.0.0"; Assets = , @{ Platform = "Windows"; File = "hl-1.0-win32.zip" } } - ) -} diff --git a/src/Main.psm1 b/src/Main.psm1 deleted file mode 100644 index 522a58c..0000000 --- a/src/Main.psm1 +++ /dev/null @@ -1,166 +0,0 @@ -using namespace System.Diagnostics.CodeAnalysis -using module ./Platform.psm1 -using module ./Release.psm1 -using module ./Setup.psm1 - -<# -.SYNOPSIS - Finds a release that matches the specified version constraint. -.PARAMETER Constraint - The version constraint. -.INPUTS - A string that contains a version constraint. -.OUTPUTS - The release corresponding to the specified constraint, or `$null` if not found. -#> -function Find-Release { - [CmdletBinding()] - [OutputType([Release])] - param ( - [Parameter(Mandatory, Position = 0, ValueFromPipeline)] - [string] $Constraint - ) - - process { - [Release]::Find($Constraint) - } -} - -<# -.SYNOPSIS - Gets the release corresponding to the specified version. -.PARAMETER Version - The version number. Use `*` or `Latest` to get the latest release. -.INPUTS - A string that contains a version number. -.OUTPUTS - The release corresponding to the specified version, or `$null` if not found. -#> -function Get-Release { - [CmdletBinding()] - [OutputType([Release])] - param ( - [Parameter(Mandatory, Position = 0, ValueFromPipeline)] - [string] $Version - ) - - process { - $Version -in "*", "Latest" ? [Release]::Latest() : [Release]::Get($Version) - } -} - -<# -.SYNOPSIS - Installs the HashLink VM, after downloading it. -.PARAMETER Version - The version number of the release to be installed. -.PARAMETER InputObject - The instance of the release to be installed. -.INPUTS - [string] A string that contains a version number. -.INPUTS - [Release] An instance of the `Release` class to be installed. -.OUTPUTS - The path to the installation directory. -#> -function Install-Release { - [CmdletBinding(DefaultParameterSetName = "Version")] - [OutputType([string])] - param ( - [Parameter(Mandatory, ParameterSetName = "Version", Position = 0, ValueFromPipeline)] - [string] $Version, - - [Parameter(Mandatory, ParameterSetName = "InputObject", ValueFromPipeline)] - [Release] $InputObject - ) - - process { - $release = $PSCmdlet.ParameterSetName -eq "InputObject" ? $InputObject : [Release]::new($Version) - [Setup]::new($release).Install() - } -} - -<# -.SYNOPSIS - Creates a new release. -.PARAMETER Version - The version number. -.PARAMETER Assets - The associated assets. -.INPUTS - A string that contains a version number. -.OUTPUTS - The newly created release. -#> -function New-Release { - [CmdletBinding()] - [OutputType([Release])] - [SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "")] - param ( - [Parameter(Mandatory, Position = 0, ValueFromPipeline)] - [string] $Version, - - [Parameter(Position = 1)] - [ReleaseAsset[]] $Assets = @() - ) - - process { - [Release]::new($Version, $Assets) - } -} - -<# -.SYNOPSIS - Creates a new release asset. -.PARAMETER Platform - The target platform. -.PARAMETER File - The target file. -.OUTPUTS - The newly created release asset. -#> -function New-ReleaseAsset { - [CmdletBinding()] - [OutputType([ReleaseAsset])] - [SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "")] - param ( - [Parameter(Mandatory, Position = 0)] - [Platform] $Platform, - - [Parameter(Mandatory, Position = 1)] - [string] $File - ) - - [ReleaseAsset]::new($Platform, $File) -} - -<# -.SYNOPSIS - Gets a value indicating whether a release with the specified version exists. -.PARAMETER Version - The version number of the release to be tested. -.PARAMETER InputObject - The instance of the release to be tested. -.INPUTS - [string] A string that contains a version number. -.INPUTS - [Release] An instance of the `Release` class to be tested. -.OUTPUTS - `$true` if a release with the specified version exists, otherwise `$false`. -#> -function Test-Release { - [CmdletBinding(DefaultParameterSetName = "Version")] - [OutputType([bool])] - param ( - [Parameter(Mandatory, ParameterSetName = "Version", Position = 0, ValueFromPipeline)] - [string] $Version, - - [Parameter(Mandatory, ParameterSetName = "InputObject", ValueFromPipeline)] - [Release] $InputObject - ) - - process { - $release = $PSCmdlet.ParameterSetName -eq "InputObject" ? $InputObject : [Release]::new($Version) - $release.Exists() - } -} diff --git a/src/Platform.cs b/src/Platform.cs new file mode 100644 index 0000000..8e3ca32 --- /dev/null +++ b/src/Platform.cs @@ -0,0 +1,39 @@ +namespace Belin.SetupHashLink; + +/// +/// Identifies an operating system or platform. +/// +public enum Platform { + + /// + /// Specifies a Linux platform. + /// + Linux, + + /// + /// Specifies a macOS platform. + /// + MacOS, + + /// + /// Specifies a Windows platform. + /// + Windows +} + +/// +/// Provides extension members for platforms. +/// +public static class PlatformExtensions { + // TODO (.NET 10) extension(Platform) + + /// + /// Gets the current platform. + /// + /// The current platform. + public static Platform GetCurrent() => true switch { + true when OperatingSystem.IsLinux() => Platform.Linux, + true when OperatingSystem.IsMacOS() => Platform.MacOS, + _ => Platform.Windows + }; +} diff --git a/src/Platform.psm1 b/src/Platform.psm1 deleted file mode 100644 index 4824147..0000000 --- a/src/Platform.psm1 +++ /dev/null @@ -1,41 +0,0 @@ -<# -.SYNOPSIS - Identifies an operating system or platform. -#> -enum Platform { - - <# - .SYNOPSIS - Specifies a Linux platform. - #> - Linux - - <# - .SYNOPSIS - Specifies a macOS platform. - #> - MacOS - - <# - .SYNOPSIS - Specifies a Windows platform. - #> - Windows -} - -<# -.SYNOPSIS - Gets the current platform. -.OUTPUTS - The current platform. -#> -function Get-Platform { - [OutputType([Platform])] - param () - - switch ($true) { - ($IsLinux) { [Platform]::Linux; break } - ($IsMacOS) { [Platform]::MacOS; break } - default { [Platform]::Windows } - } -} diff --git a/src/Release.Asset.cs b/src/Release.Asset.cs new file mode 100644 index 0000000..a32c578 --- /dev/null +++ b/src/Release.Asset.cs @@ -0,0 +1,14 @@ +namespace Belin.SetupHashLink; + +/// +/// Represents a HashLink release. +/// +public partial class Release { + + /// + /// Represents an asset of a HashLink release. + /// + /// The target platform. + /// The target file. + public sealed record Asset(Platform Platform, string File); +} diff --git a/src/Release.Data.cs b/src/Release.Data.cs new file mode 100644 index 0000000..9e7eea6 --- /dev/null +++ b/src/Release.Data.cs @@ -0,0 +1,29 @@ +namespace Belin.SetupHashLink; + +/// +/// Represents a HashLink release. +/// +public partial class Release { + + /// + /// The list of all releases. + /// + private static readonly Release[] data = [ + new Release("1.15.0", [new Asset(Platform.Windows, "hashlink-1.15.0-win.zip")]), + new Release("1.14.0", [new Asset(Platform.Windows, "hashlink-1.14.0-win.zip")]), + new Release("1.13.0", [new Asset(Platform.Windows, "hashlink-1.13.0-win.zip")]), + new Release("1.12.0", [new Asset(Platform.Windows, "hl-1.12.0-win.zip")]), + new Release("1.11.0", [new Asset(Platform.Windows, "hl-1.11.0-win.zip")]), + new Release("1.10.0", [new Asset(Platform.Windows, "hl-1.10.0-win.zip")]), + new Release("1.9.0", [new Asset(Platform.Windows, "hl-1.9.0-win.zip")]), + new Release("1.8.0", [new Asset(Platform.Windows, "hl-1.8.0-win.zip")]), + new Release("1.7.0", [new Asset(Platform.Windows, "hl-1.7.0-win.zip")]), + new Release("1.6.0", [new Asset(Platform.Windows, "hl-1.6.0-win.zip"), new Asset(Platform.Linux, "hl-1.6.0-linux.tgz")]), + new Release("1.5.0", [new Asset(Platform.Windows, "hl-1.5.0-win.zip"), new Asset(Platform.Linux, "hl-1.5.0-linux.tgz")]), + new Release("1.4.0", [new Asset(Platform.Windows, "hl-1.4-win.zip")]), + new Release("1.3.0", [new Asset(Platform.Windows, "hl-1.3-win32.zip"), new Asset(Platform.MacOS, "hl-1.3-osx32.zip")]), + new Release("1.2.0", [new Asset(Platform.Windows, "hl-1.2-win32.zip"), new Asset(Platform.MacOS, "hl-1.2-osx.zip")]), + new Release("1.1.0", [new Asset(Platform.Windows, "hl-1.1-win32.zip")]), + new Release("1.0.0", [new Asset(Platform.Windows, "hl-1.0-win32.zip")]) + ]; +} diff --git a/src/Release.cs b/src/Release.cs new file mode 100644 index 0000000..3663875 --- /dev/null +++ b/src/Release.cs @@ -0,0 +1,151 @@ +namespace Belin.SetupHashLink; + +using System.Linq; +using System.Text.RegularExpressions; + +/// +/// Represents a HashLink release. +/// +/// The version number. +/// The associated assets. +public partial class Release(Version version, IEnumerable? assets = null): IEquatable { + + /// + /// The latest release. + /// + public static Release Latest => data.First(); + + /// + /// Gets the regular expression used to check if a version number represents the latest release. + /// + /// The regular expression used to check if a version number represents the latest release. + [GeneratedRegex(@"^(\*|latest)$", RegexOptions.IgnoreCase)] + internal static partial Regex LatestReleasePattern(); + + /// + /// The associated assets. + /// + public IEnumerable Assets => assets ?? []; + + /// + /// Value indicating whether this release exists. + /// + public bool Exists => data.Any(release => release == this); + + /// + /// Value indicating whether this release is provided as source code. + /// + public bool IsSource => GetAsset(PlatformExtensions.GetCurrent()) is null; + + /// + /// The associated Git tag. + /// + public string Tag => Version.ToString(Version.Build > 0 ? 3 : 2); + + /// + /// The download URL. + /// + public Uri Url { + get { + var asset = GetAsset(PlatformExtensions.GetCurrent()); + var baseUrl = new Uri("https://github.com/HaxeFoundation/hashlink/"); + return new(baseUrl, asset is null ? $"archive/refs/tags/{Tag}.zip" : $"releases/download/{Tag}/{asset.File}"); + } + } + + /// + /// The version number. + /// + public Version Version => version; + + /// + /// Creates a new release. + /// + /// The version number. + /// The associated assets. + public Release(string version, IEnumerable? assets = null): this(Version.Parse(version), assets) {} + + /// + /// Determines whether the two specified objects are equal. + /// + /// The first object. + /// The second object. + /// if object1 equals object2, otherwise . + public static bool operator ==(Release? object1, Release? object2) => + object1 is null ? object2 is null : ReferenceEquals(object1, object2) || object1.Equals(object2); + + /// + /// Determines whether the two specified objects are not equal. + /// + /// The first object. + /// The second object. + /// if object1 does not equal object2, otherwise . + public static bool operator !=(Release? object1, Release? object2) => !(object1 == object2); + + /// + /// Finds a release that matches the specified version constraint. + /// + /// The version constraint. + /// The release corresponding to the specified constraint, or if not found. + /// The version constraint is invalid. + public static Release? Find(string constraint) { + var operatorMatch = Regex.Match(constraint, @"^([^\d]+)\d"); + var (op, version) = true switch { + true when LatestReleasePattern().IsMatch(constraint) => ("=", Latest.Version.ToString()), + true when operatorMatch.Success => (operatorMatch.Groups[1].Value, Regex.Replace(constraint, @"^[^\d]+", "")), + true when Regex.IsMatch(constraint, @"^\d") => (">=", constraint), + _ => throw new FormatException("The version constraint is invalid.") + }; + + var semver = SemanticVersion.Parse(version); + return data.FirstOrDefault(op switch { + ">" => release => new SemanticVersion(release.Version) > semver, + ">=" => release => new SemanticVersion(release.Version) >= semver, + "=" => release => new SemanticVersion(release.Version) == semver, + "<=" => release => new SemanticVersion(release.Version) <= semver, + "<" => release => new SemanticVersion(release.Version) < semver, + _ => throw new FormatException("The version constraint is invalid.") + }); + } + + /// + /// Gets the release corresponding to the specified version. + /// + /// The version number of a release. + /// The release corresponding to the specified version, or if not found. + public static Release? Get(string version) => Get(Version.Parse(version)); + + /// + /// Gets the release corresponding to the specified version. + /// + /// The version number of a release. + /// The release corresponding to the specified version, or if not found. + public static Release? Get(Version version) => data.SingleOrDefault(release => release.Version == version); + + /// + /// Determines whether the specified object is equal to this object. + /// + /// An object to compare with this object. + /// if the specified object is equal to this object, otherwise . + public override bool Equals(object? other) => Equals(other as Release); + + /// + /// Determines whether the specified object is equal to this object. + /// + /// An object to compare with this object. + /// if the specified object is equal to this object, otherwise . + public bool Equals(Release? other) => other is not null && Version == other.Version; + + /// + /// Gets the asset corresponding to the specified platform. + /// + /// The target platform. + /// The asset corresponding to the specified platform, or if not found. + public Asset? GetAsset(Platform platform) => Assets.SingleOrDefault(asset => asset.Platform == platform); + + /// + /// Gets the hash code for this object. + /// + /// The hash code for this object. + public override int GetHashCode() => HashCode.Combine(Version); +} diff --git a/src/Release.psm1 b/src/Release.psm1 deleted file mode 100644 index 0b2fd34..0000000 --- a/src/Release.psm1 +++ /dev/null @@ -1,202 +0,0 @@ -using namespace System.Diagnostics.CodeAnalysis -using module ./Platform.psm1 - -<# -.SYNOPSIS - Represents a HashLink release. -#> -class Release { - - <# - .SYNOPSIS - The list of all releases. - #> - hidden static [Release[]] $Data - - <# - .SYNOPSIS - The associated assets. - #> - [ValidateNotNull()] - [ReleaseAsset[]] $Assets - - <# - .SYNOPSIS - The version number. - #> - [ValidateNotNull()] - [semver] $Version - - <# - .SYNOPSIS - Creates a new release. - .PARAMETER Version - The version number. - #> - Release([string] $Version) { - $this.Assets = @() - $this.Version = $Version - } - - <# - .SYNOPSIS - Creates a new release. - .PARAMETER Version - The version number. - .PARAMETER Assets - The associated assets. - #> - Release([string] $Version, [ReleaseAsset[]] $Assets) { - $this.Assets = $Assets - $this.Version = $Version - } - - <# - .SYNOPSIS - Initializes the class. - #> - static Release() { - [Release]::Data = (Import-PowerShellDataFile "$PSScriptRoot/Data.psd1").Releases.ForEach{ - [Release]::new($_.Version, $_.Assets.ForEach{ [ReleaseAsset]::new($_.Platform, $_.File) }) - } - } - - <# - .SYNOPSIS - Gets a value indicating whether this release exists. - .OUTPUTS - `$true` if this release exists, otherwise `$false`. - #> - [bool] Exists() { - return $null -ne [Release]::Get($this.Version) - } - - <# - .SYNOPSIS - Gets the asset corresponding to the specified platform. - .PARAMETER Platform - The target platform. - .OUTPUTS - The asset corresponding to the specified platform, or `$null` if not found. - #> - [ReleaseAsset] GetAsset([Platform] $Platform) { - return $this.Assets.Where({ $_.Platform -eq $Platform }, "First")[0] - } - - <# - .SYNOPSIS - Gets a value indicating whether this release is provided as source code. - .OUTPUTS - `$true` if this release is provided as source code, otherwise `$false`. - #> - [bool] IsSource() { - return -not $this.GetAsset((Get-Platform)) - } - - <# - .SYNOPSIS - Gets the associated Git tag. - .OUTPUTS - The associated Git tag. - #> - [string] Tag() { - $major, $minor, $patch = $this.Version.Major, $this.Version.Minor, $this.Version.Patch - return $patch -gt 0 ? "$major.$minor.$patch" : "$major.$minor" - } - - <# - .SYNOPSIS - Gets the download URL. - .OUTPUTS - The download URL. - #> - [uri] Url() { - $asset = $this.GetAsset((Get-Platform)) - $baseUrl = [uri] "https://github.com/HaxeFoundation/hashlink/" - return [uri]::new($baseUrl, $asset ? "releases/download/$($this.Tag())/$($asset.File)" : "archive/refs/tags/$($this.Tag()).zip") - } - - <# - .SYNOPSIS - Finds a release that matches the specified version constraint. - .PARAMETER Constraint - The version constraint. - .OUTPUTS - The release corresponding to the specified constraint, or `$null` if not found. - #> - static [Release] Find([string] $Constraint) { - $operator, $semver = switch -Regex ($Constraint) { - "^(\*|latest)$" { "=", [Release]::Latest().Version; break } - "^([^\d]+)\d" { $Matches[1], [semver] ($Constraint -replace "^[^\d]+", ""); break } - "^\d" { ">=", [semver] $Constraint; break } - default { throw [FormatException] "The version constraint is invalid." } - } - - $predicate = switch ($operator) { - ">=" { { $_.Version -ge $semver }; break } - ">" { { $_.Version -gt $semver }; break } - "<=" { { $_.Version -le $semver }; break } - "<" { { $_.Version -lt $semver }; break } - "=" { { $_.Version -eq $semver }; break } - default { throw [FormatException] "The version constraint is invalid." } - } - - return [Release]::Data.Where($predicate)[0] - } - - <# - .SYNOPSIS - Gets the release corresponding to the specified version. - .PARAMETER Version - The version number of a release. - .OUTPUTS - The release corresponding to the specified version, or `$null` if not found. - #> - static [Release] Get([string] $Version) { - return [Release]::Data.Where({ $_.Version -eq $Version }, "First")[0] - } - - <# - .SYNOPSIS - Gets the latest release. - .OUTPUTS - The latest release, or `$null` if not found. - #> - static [Release] Latest() { - return [Release]::Data[0] - } -} - -<# -.SYNOPSIS - Represents an asset of a HashLink release. -#> -class ReleaseAsset { - - <# - .SYNOPSIS - The target file. - #> - [ValidateNotNullOrWhiteSpace()] - [string] $File - - <# - .SYNOPSIS - The target platform. - #> - [ValidateNotNull()] - [Platform] $Platform - - <# - .SYNOPSIS - Creates a new release asset. - .PARAMETER Platform - The target platform. - .PARAMETER File - The target file. - #> - ReleaseAsset([Platform] $Platform, [string] $File) { - $this.File = $File - $this.Platform = $Platform - } -} diff --git a/src/Setup.cs b/src/Setup.cs new file mode 100644 index 0000000..90ac66e --- /dev/null +++ b/src/Setup.cs @@ -0,0 +1,135 @@ +namespace Belin.SetupHashLink; + +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Threading; + +/// +/// Manages the download and installation of the HashLink VM. +/// +/// The release to download and install. +public class Setup(Release release) { + + /// + /// The release to download and install. + /// + public Release Release => release; + + /// + /// Downloads and extracts the ZIP archive of the HashLink VM. + /// + /// The token to cancel the operation. + /// The path to the extracted directory. + public async Task Download(CancellationToken cancellationToken = default) { + using var httpClient = new HttpClient(); + var version = GetType().Assembly.GetName().Version!; + httpClient.DefaultRequestHeaders.Add("User-Agent", $".NET/{Environment.Version.ToString(3)} | SetupHashLink/{version.ToString(3)}"); + + var bytes = await httpClient.GetByteArrayAsync(Release.Url, cancellationToken); + var file = Path.GetTempFileName(); + await File.WriteAllBytesAsync(file, bytes, cancellationToken); + + var directory = Path.Join(Path.GetTempPath(), Guid.NewGuid().ToString()); + // TODO (.NET 10) await ZipFile.ExtractToDirectoryAsync(file, directory, cancellationToken); + ZipFile.ExtractToDirectory(file, directory); + return Path.Join(directory, Path.GetFileName(Directory.EnumerateDirectories(directory).Single())); + } + + /// + /// Installs the HashLink VM, after downloading it. + /// + /// The token to cancel the operation. + /// The path to the installation directory. + public async Task Install(CancellationToken cancellationToken = default) { + var directory = await Download(cancellationToken); + if (Release.IsSource && Environment.GetEnvironmentVariable("CI") is not null) await Compile(directory, cancellationToken); + + var binFolder = Release.IsSource ? Path.Join(directory, "bin") : directory; + Environment.SetEnvironmentVariable("PATH", $"{Environment.GetEnvironmentVariable("PATH")}{Path.PathSeparator}{binFolder}"); + await File.AppendAllTextAsync(Environment.GetEnvironmentVariable("GITHUB_PATH")!, binFolder, cancellationToken); + return directory; + } + + /// + /// Compiles the sources of the HashLink VM located in the specified directory. + /// + /// The path to the directory containing the HashLink sources. + /// The token to cancel the operation. + /// The path to the output directory. + /// The compilation is not supported on Windows platform. + private async Task Compile(string directory, CancellationToken cancellationToken) { + var platform = PlatformExtensions.GetCurrent(); + if (platform == Platform.Windows) throw new PlatformNotSupportedException("Compilation is not supported on Windows platform."); + + var workingDirectory = Environment.CurrentDirectory; + Environment.CurrentDirectory = directory; + var path = platform == Platform.MacOS ? await CompileMacOS(cancellationToken) : await CompileLinux(cancellationToken); + Environment.CurrentDirectory = workingDirectory; + return path; + } + + /// + /// Compiles the HashLink sources on the Linux platform. + /// + /// The path to the output directory. + /// An error occurred while executing a native command. + private static async Task CompileLinux(CancellationToken cancellationToken) { + var dependencies = new[] { + "libglu1-mesa-dev", + "libmbedtls-dev", + "libopenal-dev", + "libpng-dev", + "libsdl2-dev", + "libsqlite3-dev", + "libturbojpeg-dev", + "libuv1-dev", + "libvorbis-dev" + }; + + var commands = new[] { + ("sudo", "apt-get update"), + ("sudo", $"apt-get install --assume-yes --no-install-recommends {string.Join(" ", dependencies)}"), + ("make", ""), + ("sudo", "make install"), + ("sudo", "ldconfig") + }; + + foreach (var (fileName, arguments) in commands) { + using var process = Process.Start(fileName, arguments) ?? throw new ApplicationFailedException(fileName); + await process.WaitForExitAsync(cancellationToken); + if (process.ExitCode != 0) throw new ApplicationFailedException(fileName); + } + + var prefix = "/usr/local"; + var ldLibraryPath = $"{Environment.GetEnvironmentVariable("LD_LIBRARY_PATH")}{Path.PathSeparator}{prefix}/bin"; + Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", ldLibraryPath); + await File.AppendAllTextAsync(Environment.GetEnvironmentVariable("GITHUB_ENV")!, $"LD_LIBRARY_PATH={ldLibraryPath}", cancellationToken); + return prefix; + } + + /// + /// Compiles the HashLink sources on the macOS platform. + /// + /// The token to cancel the operation. + /// The path to the output directory. + /// An error occurred while executing a native command. + private static async Task CompileMacOS(CancellationToken cancellationToken) { + var prefix = "/usr/local"; + var commands = new[] { + ("brew", "bundle"), + ("make", ""), + ("sudo", "make codesign_osx"), + ("sudo", "make install"), + ("sudo", $"install_name_tool -change libhl.dylib {prefix}/lib/libhl.dylib {prefix}/bin/hl") + }; + + foreach (var (fileName, arguments) in commands) { + using var process = Process.Start(fileName, arguments) ?? throw new ApplicationFailedException(fileName); + await process.WaitForExitAsync(cancellationToken); + if (process.ExitCode != 0) throw new ApplicationFailedException(fileName); + } + + return prefix; + } +} diff --git a/src/Setup.psm1 b/src/Setup.psm1 deleted file mode 100644 index 35b8395..0000000 --- a/src/Setup.psm1 +++ /dev/null @@ -1,146 +0,0 @@ -using namespace System.Diagnostics.CodeAnalysis -using namespace System.IO -using module ./Platform.psm1 -using module ./Release.psm1 - -<# -.SYNOPSIS - Manages the download and installation of the HashLink VM. -#> -class Setup { - - <# - .SYNOPSIS - The release to download and install. - #> - [ValidateNotNull()] - hidden [Release] $Release - - <# - .SYNOPSIS - Creates a new setup. - .PARAMETER Release - The release to download and install. - #> - Setup([Release] $Release) { - $this.Release = $Release - } - - <# - .SYNOPSIS - Downloads and extracts the ZIP archive of the HashLink VM. - .OUTPUTS - The path to the extracted directory. - #> - [string] Download() { - $file = New-TemporaryFile - Invoke-WebRequest $this.Release.Url() -OutFile $file - $directory = Join-Path ([Path]::GetTempPath()) (New-Guid) - Expand-Archive $file $directory -Force - return Join-Path $directory $this.FindSubfolder($directory) - } - - <# - .SYNOPSIS - Installs the HashLink VM, after downloading it. - .OUTPUTS - The path to the installation directory. - #> - [string] Install() { - $directory = $this.Download() - $isSource = $this.Release.IsSource() - if ($isSource -and $Env:CI) { $this.Compile($directory) } - - $binFolder = $isSource ? (Join-Path $directory "bin") : $directory - $Env:PATH += "$([Path]::PathSeparator)$binFolder" - Add-Content $Env:GITHUB_PATH $binFolder - return $directory - } - - <# - .SYNOPSIS - Compiles the sources of the HashLink VM located in the specified directory. - .PARAMETER Directory - The path to the directory containing the HashLink sources. - .OUTPUTS - The path to the output directory. - #> - hidden [string] Compile([string] $Directory) { - $platform = Get-Platform - if ($platform -eq [Platform]::Windows) { throw [PlatformNotSupportedException] "Compilation is not supported on Windows platform." } - - $workingDirectory = Get-Location - Set-Location $Directory - $path = $platform -eq [Platform]::MacOS ? $this.CompileMacOS() : $this.CompileLinux() - Set-Location $workingDirectory - return $path - } - - <# - .SYNOPSIS - Compiles the HashLink sources on the Linux platform. - .OUTPUTS - The path to the output directory. - #> - hidden [string] CompileLinux() { - $dependencies = @( - "libglu1-mesa-dev" - "libmbedtls-dev" - "libopenal-dev" - "libpng-dev" - "libsdl2-dev" - "libsqlite3-dev" - "libturbojpeg-dev" - "libuv1-dev" - "libvorbis-dev" - ) - - sudo apt-get update - sudo apt-get install --assume-yes --no-install-recommends @dependencies - make - sudo make install - sudo ldconfig - - $prefix = "/usr/local" - $binFolder = Join-Path $prefix "bin" - $Env:LD_LIBRARY_PATH += "$([Path]::PathSeparator)$binFolder" - Add-Content $Env:GITHUB_ENV "LD_LIBRARY_PATH=$Env:LD_LIBRARY_PATH" - return $prefix - } - - <# - .SYNOPSIS - Compiles the HashLink sources on the macOS platform. - .OUTPUTS - The path to the output directory. - #> - hidden [string] CompileMacOS() { - $prefix = "/usr/local" - - brew bundle - make - sudo make codesign_osx - sudo make install - sudo install_name_tool -change libhl.dylib $prefix/lib/libhl.dylib $prefix/bin/hl - - return $prefix - } - - <# - .SYNOPSIS - Determines the name of the single subfolder in the specified directory. - .PARAMETER Directory - The directory path. - .OUTPUTS - The name of the single subfolder in the specified directory. - #> - [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] - hidden [string] FindSubfolder([string] $Directory) { - $folders = Get-ChildItem $Directory -Directory - return $discard = switch ($folders.Count) { - 0 { throw [DirectoryNotFoundException] "No subfolder found in: $Directory." } - 1 { $folders[0].BaseName; break } - default { throw [DirectoryNotFoundException] "Multiple subfolders found in: $Directory." } - } - } -} diff --git a/src/SetupHashLink.csproj b/src/SetupHashLink.csproj new file mode 100644 index 0000000..425debc --- /dev/null +++ b/src/SetupHashLink.csproj @@ -0,0 +1,27 @@ + + + Cedric-Belin.fr + © Cédric Belin + Set up your GitHub Actions workflow with a specific version of the HashLink VM. + Setup HashLink VM + 8.0.0 + + + + false + Belin.SetupHashLink + true + enable + enable + ../bin + net8.0 + + + + + + + + + + diff --git a/src/SetupHashLink.esproj b/src/SetupHashLink.esproj deleted file mode 100644 index a2c426d..0000000 --- a/src/SetupHashLink.esproj +++ /dev/null @@ -1,7 +0,0 @@ - - - ../ - false - false - - diff --git a/test/AssemblyInfo.cs b/test/AssemblyInfo.cs new file mode 100644 index 0000000..300f5b1 --- /dev/null +++ b/test/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/test/Cmdlets/BeforeAll.ps1 b/test/Cmdlets/BeforeAll.ps1 new file mode 100644 index 0000000..c838295 --- /dev/null +++ b/test/Cmdlets/BeforeAll.ps1 @@ -0,0 +1,15 @@ +using namespace System.Diagnostics.CodeAnalysis +Import-Module "$PSScriptRoot/../../SetupHashLink.psd1" + +[SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] +$existingRelease = New-HashLinkRelease "1.15.0" @( + New-HashLinkReleaseAsset Linux "hashlink-1.15.0.zip" + New-HashLinkReleaseAsset MacOS "hashlink-1.15.0.zip" + New-HashLinkReleaseAsset Windows "hashlink-1.15.0.zip" +) + +[SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] +$latestRelease = Get-HashLinkRelease "Latest" + +[SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] +$nonExistingRelease = New-HashLinkRelease "666.6.6" diff --git a/test/Cmdlets/Find-Release.Tests.ps1 b/test/Cmdlets/Find-Release.Tests.ps1 new file mode 100644 index 0000000..f9010e4 --- /dev/null +++ b/test/Cmdlets/Find-Release.Tests.ps1 @@ -0,0 +1,28 @@ +<# +.SYNOPSIS + Tests the features of the `Find-Release` cmdlet. +#> +Describe "Find-Release" { + BeforeAll { + . "$PSScriptRoot/BeforeAll.ps1" + } + + It "should return `$null if no release matches the version constraint" { + Find-HashLinkRelease $nonExistingRelease.Version | Should -Be $null + } + + It "should return the release corresponding to the version constraint if it exists" { + Find-HashLinkRelease "latest" | Should -Be $latestRelease + Find-HashLinkRelease "*" | Should -Be $latestRelease + Find-HashLinkRelease "1" | Should -Be $latestRelease + Find-HashLinkRelease "2" | Should -Be $null + (Find-HashLinkRelease ">1.15")?.Version | Should -Be $null + (Find-HashLinkRelease "=1.8")?.Version | Should -Be "1.8.0" + (Find-HashLinkRelease "<1.10")?.Version | Should -Be "1.9.0" + (Find-HashLinkRelease "<=1.10")?.Version | Should -Be "1.10.0" + } + + It "should throw if the version constraint is invalid" -TestCases @{ Version = "abc" }, @{ Version = "?1.10" } { + { Find-HashLinkRelease $version } | Should -Throw + } +} diff --git a/test/Cmdlets/Get-Release.Tests.ps1 b/test/Cmdlets/Get-Release.Tests.ps1 new file mode 100644 index 0000000..4ac39af --- /dev/null +++ b/test/Cmdlets/Get-Release.Tests.ps1 @@ -0,0 +1,17 @@ +<# +.SYNOPSIS + Tests the features of the `Get-Release` cmdlet. +#> +Describe "Get-Release" { + BeforeAll { + . "$PSScriptRoot/BeforeAll.ps1" + } + + It "should return `$null if no release matches to the version number" { + Get-HashLinkRelease $nonExistingRelease.Version | Should -Be $null + } + + It "should return the release corresponding to the version number if it exists" { + (Get-HashLinkRelease "1.8.0")?.Version | Should -Be "1.8.0" + } +} diff --git a/test/Cmdlets/Test-Release.Tests.ps1 b/test/Cmdlets/Test-Release.Tests.ps1 new file mode 100644 index 0000000..b3fb1c7 --- /dev/null +++ b/test/Cmdlets/Test-Release.Tests.ps1 @@ -0,0 +1,24 @@ +<# +.SYNOPSIS + Tests the features of the `Test-Release` cmdlet. +#> +Describe "Test-Release" { + BeforeAll { + . "$PSScriptRoot/BeforeAll.ps1" + } + + It "should return `$true for the latest release" { + Test-HashLinkRelease $latestRelease.Version | Should -BeTrue + $latestRelease | Test-HashLinkRelease | Should -BeTrue + } + + It "should return `$true if the release exists" { + Test-HashLinkRelease $existingRelease.Version | Should -BeTrue + $existingRelease | Test-HashLinkRelease | Should -BeTrue + } + + It "should return `$false if the release does not exist" { + Test-HashLinkRelease $nonExistingRelease.Version | Should -BeFalse + $nonExistingRelease | Test-HashLinkRelease | Should -BeFalse + } +} diff --git a/test/Main.Tests.ps1 b/test/Main.Tests.ps1 deleted file mode 100644 index 8a494fa..0000000 --- a/test/Main.Tests.ps1 +++ /dev/null @@ -1,72 +0,0 @@ -using namespace System.Diagnostics.CodeAnalysis - -<# -.SYNOPSIS - Tests the features of the `Main` module. -#> -Describe "Main" { - BeforeAll { - Import-Module ./SetupHashLink.psd1 - - [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] - $existingRelease = New-HashLinkRelease "1.15.0" @( - New-HashLinkReleaseAsset Linux "hashlink-1.15.0.zip" - New-HashLinkReleaseAsset MacOS "hashlink-1.15.0.zip" - New-HashLinkReleaseAsset Windows "hashlink-1.15.0.zip" - ) - - [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] - $latestRelease = Get-HashLinkRelease "Latest" - - [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] - $nonExistingRelease = New-HashLinkRelease "666.6.6" - } - - Context "Find-Release" { - It "should return `$null if no release matches the version constraint" { - Find-HashLinkRelease $nonExistingRelease.Version | Should -Be $null - } - - It "should return the release corresponding to the version constraint if it exists" { - Find-HashLinkRelease "latest" | Should -Be $latestRelease - Find-HashLinkRelease "*" | Should -Be $latestRelease - Find-HashLinkRelease "1" | Should -Be $latestRelease - Find-HashLinkRelease "2" | Should -Be $null - (Find-HashLinkRelease ">1.15")?.Version | Should -Be $null - (Find-HashLinkRelease "=1.8.0")?.Version | Should -Be "1.8.0" - (Find-HashLinkRelease "<1.10")?.Version | Should -Be "1.9.0" - (Find-HashLinkRelease "<=1.10")?.Version | Should -Be "1.10.0" - } - - It "should throw if the version constraint is invalid" -TestCases @{ Version = "abc" }, @{ Version = "?1.10" } { - { Find-HashLinkRelease $version } | Should -Throw - } - } - - Context "Get-Release" { - It "should return `$null if no release matches to the version number" { - Get-HashLinkRelease $nonExistingRelease.Version | Should -Be $null - } - - It "should return the release corresponding to the version number if it exists" { - (Get-HashLinkRelease "1.8.0")?.Version | Should -Be "1.8.0" - } - } - - Context "Test-Release" { - It "should return `$true for the latest release" { - Test-HashLinkRelease $latestRelease.Version | Should -BeTrue - $latestRelease | Test-HashLinkRelease | Should -BeTrue - } - - It "should return `$true if the release exists" { - Test-HashLinkRelease $existingRelease.Version | Should -BeTrue - $existingRelease | Test-HashLinkRelease | Should -BeTrue - } - - It "should return `$false if the release does not exist" { - Test-HashLinkRelease $nonExistingRelease.Version | Should -BeFalse - $nonExistingRelease | Test-HashLinkRelease | Should -BeFalse - } - } -} diff --git a/test/Release.Tests.cs b/test/Release.Tests.cs new file mode 100644 index 0000000..4f8a14c --- /dev/null +++ b/test/Release.Tests.cs @@ -0,0 +1,77 @@ +namespace Belin.SetupHashLink; + +/// +/// Tests the features of the class. +/// +/// The test context. +[TestClass] +public sealed class ReleaseTests { + + /// + /// A release that exists. + /// + private readonly Release existingRelease = new("1.15.0", [ + new Release.Asset(Platform.Linux, "hashlink-1.15.0.zip"), + new Release.Asset(Platform.MacOS, "hashlink-1.15.0.zip"), + new Release.Asset(Platform.Windows, "hashlink-1.15.0.zip") + ]); + + /// + /// A release that does not exist. + /// + private readonly Release nonExistingRelease = new("666.6.6"); + + [TestMethod] + public void Exists() { + IsTrue(existingRelease.Exists); + IsFalse(nonExistingRelease.Exists); + } + + [TestMethod] + public void IsSource() { + IsFalse(existingRelease.IsSource); + IsTrue(nonExistingRelease.IsSource); + } + + [TestMethod] + public void Tag() { + AreEqual("1.15", existingRelease.Tag); + AreEqual("666.6.6", nonExistingRelease.Tag); + } + + [TestMethod] + public void Url() { + AreEqual(new Uri("https://github.com/HaxeFoundation/hashlink/releases/download/1.15/hashlink-1.15.0.zip"), existingRelease.Url); + AreEqual(new Uri("https://github.com/HaxeFoundation/hashlink/archive/refs/tags/666.6.6.zip"), nonExistingRelease.Url); + } + + [TestMethod] + public void Find() { + IsNull(Release.Find(nonExistingRelease.Version.ToString())); + IsNull(Release.Find("2")); + IsNull(Release.Find(">1.15")); + + AreEqual(Release.Latest, Release.Find("latest")); + AreEqual(Release.Latest, Release.Find("*")); + AreEqual(Release.Latest, Release.Find("1")); + + AreEqual(new Release("1.8.0"), Release.Find("=1.8")); + AreEqual(new Release("1.9.0"), Release.Find("<1.10")); + AreEqual(new Release("1.10.0"), Release.Find("<=1.10")); + + Throws(() => Release.Find("abc")); + Throws(() => Release.Find("?1.10")); + } + + [TestMethod] + public void Get() { + IsNull(Release.Get(nonExistingRelease.Version)); + AreEqual(Version.Parse("1.8.0"), Release.Get("1.8.0")?.Version); + } + + [TestMethod] + public void GetAsset() { + AreEqual("hashlink-1.15.0.zip", existingRelease.GetAsset(Platform.Windows)?.File); + IsNull(nonExistingRelease.GetAsset(Platform.Windows)); + } +} diff --git a/test/Release.Tests.ps1 b/test/Release.Tests.ps1 deleted file mode 100644 index a96946b..0000000 --- a/test/Release.Tests.ps1 +++ /dev/null @@ -1,111 +0,0 @@ -using namespace System.Diagnostics.CodeAnalysis -using module ../src/Platform.psm1 -using module ../src/Release.psm1 - -<# -.SYNOPSIS - Tests the features of the `Release` module. -#> -Describe "Release" { - BeforeAll { - [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] - $existingRelease = [Release]::new("1.15.0", @( - [ReleaseAsset]::new([Platform]::Linux, "hashlink-1.15.0.zip") - [ReleaseAsset]::new([Platform]::MacOS, "hashlink-1.15.0.zip") - [ReleaseAsset]::new([Platform]::Windows, "hashlink-1.15.0.zip") - )) - - [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] - $latestRelease = [Release]::Latest() - - [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] - $nonExistingRelease = [Release] "666.6.6" - } - - Context "Exists" { - It "should return `$true if the release exists" { - $existingRelease.Exists() | Should -BeTrue - } - - It "should return `$false if the release does not exist" { - $nonExistingRelease.Exists() | Should -BeFalse - } - } - - Context "GetAsset" { - It "should return `$null if no asset matches the platform" { - $nonExistingRelease.GetAsset([Platform]::Windows) | Should -Be $null - } - - It "should return the asset corresponding to the platform number if it exists" { - $existingRelease.GetAsset([Platform]::Windows)?.File | Should -BeExactly "hashlink-1.15.0.zip" - } - } - - Context "IsSource" { - It "should return `$true if the release is provided as source code" { - $nonExistingRelease.IsSource() | Should -BeTrue - } - - It "should return `$false if the release is provided as binary" { - $existingRelease.IsSource() | Should -BeFalse - } - } - - Context "Tag" { - It "should not include the patch component if it's zero" { - $existingRelease.Tag() | Should -Be "1.15" - } - - It "should include the patch component if it's greater than zero" { - $nonExistingRelease.Tag() | Should -Be $nonExistingRelease.Version - } - } - - Context "Url" { - It "should point to a GitHub tag if the release is provided as source code" { - $nonExistingRelease.Url() | Should -BeExactly "https://github.com/HaxeFoundation/hashlink/archive/refs/tags/666.6.6.zip" - } - - It "should point to a GitHub release if the release is provided as binary" { - $existingRelease.Url() | Should -BeExactly "https://github.com/HaxeFoundation/hashlink/releases/download/1.15/hashlink-1.15.0.zip" - } - } - - Context "Find" { - It "should return `$null if no release matches the version constraint" { - [Release]::Find($nonExistingRelease.Version) | Should -Be $null - } - - It "should return the release corresponding to the version constraint if it exists" { - [Release]::Find("latest") | Should -Be $latestRelease - [Release]::Find("*") | Should -Be $latestRelease - [Release]::Find("1") | Should -Be $latestRelease - [Release]::Find("2") | Should -Be $null - [Release]::Find(">1.15")?.Version | Should -Be $null - [Release]::Find("=1.8.0")?.Version | Should -Be "1.8.0" - [Release]::Find("<1.10")?.Version | Should -Be "1.9.0" - [Release]::Find("<=1.10")?.Version | Should -Be "1.10.0" - } - - It "should throw if the version constraint is invalid" -TestCases @{ Version = "abc" }, @{ Version = "?1.10" } { - { [Release]::Find($version) } | Should -Throw - } - } - - Context "Get" { - It "should return `$null if no release matches to the version number" { - [Release]::Get($nonExistingRelease.Version) | Should -Be $null - } - - It "should return the release corresponding to the version number if it exists" { - [Release]::Get("1.8.0")?.Version | Should -Be "1.8.0" - } - } - - Context "Latest" { - It "should exist" { - $latestRelease | Should -Not -Be $null - } - } -} diff --git a/test/Setup.Tests.cs b/test/Setup.Tests.cs new file mode 100644 index 0000000..a4bf23e --- /dev/null +++ b/test/Setup.Tests.cs @@ -0,0 +1,40 @@ +namespace Belin.SetupHashLink; + +/// +/// Tests the features of the class. +/// +/// The test context. +[TestClass] +public sealed class SetupTests(TestContext testContext) { + + /// + /// The current platform. + /// + private readonly Platform platform = PlatformExtensions.GetCurrent(); + + [ClassInitialize] + public static void ClassInitialize(TestContext testContext) { + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ENV"))) Environment.SetEnvironmentVariable("GITHUB_ENV", "var/GitHub-Env.txt"); + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_PATH"))) Environment.SetEnvironmentVariable("GITHUB_PATH", "var/GitHub-Path.txt"); + } + + [TestMethod] + public async Task Download() { + var setup = new Setup(Release.Latest); + var path = await setup.Download(testContext.CancellationToken); + + var executable = $"hl{(setup.Release.IsSource ? ".vcxproj" : platform == Platform.Windows ? ".exe" : "")}"; + IsTrue(File.Exists(Path.Join(path, executable))); + + var dynamicLibrary = $"libhl{(setup.Release.IsSource ? ".vcxproj" : platform == Platform.MacOS ? ".dylib" : platform == Platform.Linux ? ".so" : ".dll")}"; + IsTrue(File.Exists(Path.Join(path, dynamicLibrary))); + } + + [TestMethod] + public async Task Install() { + var setup = new Setup(Release.Latest); + var path = await setup.Install(testContext.CancellationToken); + Contains(path, Environment.GetEnvironmentVariable("PATH")!); + if (platform == Platform.Linux && setup.Release.IsSource) Contains("/usr/local/bin", Environment.GetEnvironmentVariable("LD_LIBRARY_PATH")!); + } +} diff --git a/test/Setup.Tests.ps1 b/test/Setup.Tests.ps1 deleted file mode 100644 index 2266713..0000000 --- a/test/Setup.Tests.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -using namespace System.Diagnostics.CodeAnalysis -using module ../src/Platform.psm1 -using module ../src/Release.psm1 -using module ../src/Setup.psm1 - -<# -.SYNOPSIS - Tests the features of the `Setup` module. -#> -Describe "Setup" { - BeforeAll { - [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] - $latestRelease = [Release]::Latest() - - [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "")] - $platform = Get-Platform - - if (-not (Test-Path Env:GITHUB_ENV)) { $Env:GITHUB_ENV = "var/GitHub-Env.txt" } - if (-not (Test-Path Env:GITHUB_PATH)) { $Env:GITHUB_PATH = "var/GitHub-Path.txt" } - } - - Context "Download" { - It "should properly download and extract the HashLink VM" { - $setup = [Setup]::new($latestRelease) - $isSource = $setup.Release.IsSource() - $path = $setup.Download() - - $executable = "hl$($isSource ? ".vcxproj" : $platform -eq [Platform]::Windows ? ".exe" : [string]::Empty)" - Join-Path $path $executable | Should -Exist - - $dynamicLib = "libhl$($isSource ? ".vcxproj" : $platform -eq [Platform]::MacOS ? ".dylib" : $platform -eq [Platform]::Linux ? ".so" : ".dll")" - Join-Path $path $dynamicLib | Should -Exist - } - } - - Context "Install" { - It "should add the HashLink VM binaries to the PATH environment variable" { - $setup = [Setup]::new($latestRelease) - $path = $setup.Install() - - $Env:PATH | Should -BeLikeExactly "*$path*" - if (($platform -eq [Platform]::Linux) -and ($setup.Release.IsSource())) { - $Env:LD_LIBRARY_PATH | Should -BeLikeExactly "*/usr/local/bin*" - } - } - } -} diff --git a/test/SetupHashLink.Tests.csproj b/test/SetupHashLink.Tests.csproj new file mode 100644 index 0000000..d264ead --- /dev/null +++ b/test/SetupHashLink.Tests.csproj @@ -0,0 +1,30 @@ + + + Cedric-Belin.fr + © Cédric Belin + Setup HashLink VM + 8.0.0 + + + + false + Belin.SetupHashLink.Tests + obj + enable + enable + ../bin + en + net8.0 + + + + + + + + + + + + + diff --git a/test/SetupHashLink.Tests.esproj b/test/SetupHashLink.Tests.esproj deleted file mode 100644 index a2c426d..0000000 --- a/test/SetupHashLink.Tests.esproj +++ /dev/null @@ -1,7 +0,0 @@ - - - ../ - false - false - - diff --git a/tool/Build.ps1 b/tool/Build.ps1 new file mode 100644 index 0000000..0b76275 --- /dev/null +++ b/tool/Build.ps1 @@ -0,0 +1,2 @@ +"Building the solution..." +dotnet build --configuration ($Release ? "Release" : "Debug") diff --git a/tool/Clean.ps1 b/tool/Clean.ps1 index 7a9909c..ffc27f0 100644 --- a/tool/Clean.ps1 +++ b/tool/Clean.ps1 @@ -1,2 +1,4 @@ "Deleting all generated files..." +Remove-Item "bin" -ErrorAction Ignore -Force -Recurse +Remove-Item "*/obj" -Force -Recurse Remove-Item "var/*" -Exclude ".gitkeep" -Force -Recurse diff --git a/tool/Default.ps1 b/tool/Default.ps1 index 4e1a44f..b3a7a6a 100644 --- a/tool/Default.ps1 +++ b/tool/Default.ps1 @@ -1 +1,3 @@ & "$PSScriptRoot/Clean.ps1" +& "$PSScriptRoot/Version.ps1" +& "$PSScriptRoot/Build.ps1" diff --git a/tool/Format.ps1 b/tool/Format.ps1 new file mode 100644 index 0000000..d968024 --- /dev/null +++ b/tool/Format.ps1 @@ -0,0 +1,2 @@ +"Formatting the source code..." +dotnet format diff --git a/tool/Lint.ps1 b/tool/Lint.ps1 index 1eb8128..fc73aae 100644 --- a/tool/Lint.ps1 +++ b/tool/Lint.ps1 @@ -1,6 +1,5 @@ "Performing the static analysis of source code..." Import-Module PSScriptAnalyzer Invoke-ScriptAnalyzer $PSScriptRoot -Recurse -Invoke-ScriptAnalyzer src -Recurse Invoke-ScriptAnalyzer test -Recurse Test-ModuleManifest SetupHashLink.psd1 | Out-Null diff --git a/tool/Outdated.ps1 b/tool/Outdated.ps1 new file mode 100644 index 0000000..be4b30f --- /dev/null +++ b/tool/Outdated.ps1 @@ -0,0 +1,2 @@ +"Checking for outdated dependencies..." +dotnet package list --outdated diff --git a/tool/Publish.ps1 b/tool/Publish.ps1 index fe45655..5e242e9 100644 --- a/tool/Publish.ps1 +++ b/tool/Publish.ps1 @@ -1,4 +1,9 @@ -& "$PSScriptRoot/Default.ps1" +if ($Release) { & "$PSScriptRoot/Default.ps1" } +else { + "The ""-Release"" switch must be set!" + exit 1 +} + "Publishing the package..." $version = (Import-PowerShellDataFile "SetupHashLink.psd1").ModuleVersion diff --git a/tool/Test.ps1 b/tool/Test.ps1 index 226e684..95b5db9 100644 --- a/tool/Test.ps1 +++ b/tool/Test.ps1 @@ -1,4 +1,5 @@ "Running the test suite..." +dotnet test --settings .runsettings pwsh -Command { Import-Module Pester Invoke-Pester test diff --git a/tool/Version.ps1 b/tool/Version.ps1 new file mode 100644 index 0000000..38844ff --- /dev/null +++ b/tool/Version.ps1 @@ -0,0 +1,5 @@ +"Updating the version number in the sources..." +$version = (Import-PowerShellDataFile "SetupHashLink.psd1").ModuleVersion +foreach ($item in Get-Item "*/*.csproj") { + (Get-Content $item) -replace "\d+(\.\d+){2}", "$version" | Out-File $item +} diff --git a/tool/Watch.ps1 b/tool/Watch.ps1 new file mode 100644 index 0000000..7582a8b --- /dev/null +++ b/tool/Watch.ps1 @@ -0,0 +1,3 @@ +"Watching for file changes..." +$configuration = $Release ? "Release" : "Debug" +Start-Process dotnet "watch build --configuration $configuration" -NoNewWindow -Wait -WorkingDirectory src