Includes/PwSh.Fw.Build.Windows.NSIS.psm1
if (!($Script:PWSHFW_BUILDHELPERS_DIR)) { $Script:PWSHFW_BUILDHELPERS_DIR = (Resolve-Path $PSScriptRoot/../).Path } # @var ConvertIsInstalled # @brief $true if 'convert' from Image Magick package is installed # $Script:ConvertIsInstalled = Get-Command -Name "convert" -ErrorAction SilentlyContinue # on Windows, convert.exe is a Microsoft tool to convert partition filesystem. So 'Get-Command convert.exe' will always return something... we don't want # instead we'll search for a convert.exe tool installed in ProgramFiles # @(()) means force use of an array, even if only one is found. [-1] means take the last. $Script:ConvertExe = @((Get-ChildItem -path "$env:ProgramFiles" -name "convert.exe" -Recurse | ForEach-Object { (Get-Item "$env:ProgramFiles\$_").FullName }))[-1] $Script:ConvertIsInstalled = Test-FileExist "$Script:ConvertExe" $Script:ConvertNotInstalledMessage = "'convert.exe' command not found. Automatic icon handling is disabled. Please read the FAQ if you need it." $Script:Png2icnsIsInstalled = Get-Command -Name "png2icns" -ErrorAction SilentlyContinue $Script:Png2icnsNotInstalledMessage = "'png2icns' command not found. Automatic icon handling is disabled. Please read the FAQ if you need it." <# .SYNOPSIS Convert a generic hashtable into useful NSIS Settings metadata .DESCRIPTION Extract from an object useful properties to use as NSIS constants .PARAMETER Metadata object filled with various properties .EXAMPLE $project = gc ./project.yml -raw | convertfrom-yaml $project | ConvertTo-NSISSettings This example will convert a project definition file into a useable hashtable to inject into Out-NSISSetupFile .NOTES General notes #> function ConvertTo-NSISSettings { [CmdletBinding()][OutputType([hashtable])]Param ( [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$Metadata ) Begin { } Process { $NSISSettings = @{} if ($Metadata) { if ($Metadata.name) { $NSISSettings.Name = $Metadata.name } # override package name with optional PackageName attribute if ($Metadata.PackageName) { $NSISSettings.Package = $Metadata.PackageName } if ($Metadata.DisplayName) { $NSISSettings.DisplayName = $Metadata.DisplayName } if ($Metadata.Codename) { $NSISSettings.CodeName = $Metadata.Codename } if ($Metadata.Version) { $NSISSettings.Version = $Metadata.Version } if ($Metadata.GUID) { $NSISSettings.GUID = $Metadata.GUID } if ($Metadata.Authors) { $NSISSettings.Author = $Metadata.Authors[0] } if ($Metadata.Author) { $NSISSettings.Author = $Metadata.Author } if ($Metadata.owner) { $NSISSettings.Author = $Metadata.owner } if ($Metadata.CompanyName) { $NSISSettings.CompanyName = $Metadata.CompanyName } if ($Metadata.Copyright) { $NSISSettings.Copyright = $Metadata.Copyright } if ($Metadata.Description) { $NSISSettings.Description = $Metadata.Description } if ($Metadata.ProcessorArchitecture) { $NSISSettings.Architecture = $Metadata.ProcessorArchitecture } if ($Metadata.Architecture) { $NSISSettings.Architecture = $Metadata.Architecture } if ($Metadata.Arch) { $NSISSettings.Architecture = $Metadata.Arch } if ($Metadata.ProjectUri) { $NSISSettings.ProjectUri = $Metadata.ProjectUri } if ($Metadata.ProjectUrl) { $NSISSettings.ProjectUri = $Metadata.ProjectUrl } if ($Metadata.LicenseFile) { $NSISSettings.LicenseFile = (Resolve-Path $Metadata.LicenseFile -Relative) } if ($Metadata.IconFile) { if (Test-FileExist $Metadata.IconFile) { $NSISSettings.IconFile = (Resolve-Path $Metadata.IconFile -Relative) } else { Write-Warning "Icon file '$($Metadata.IconFile) not found. Ignore icon setting." $Metadata.IconFile = $null } } # if ([string]::IsNullOrEmpty($Metadata.IconFile)) { # # makensis seems to not like mess with bask-slashes and forward-slashes # # $NSISSettings.IconFile = "$($Script:PWSHFW_BUILDHELPERS_DIR)/Assets/application.png" # $NSISSettings.IconFile = (Resolve-Path "$($Script:PWSHFW_BUILDHELPERS_DIR)/Assets/application.png").Path # } if ($Metadata.IconBasename) { $NSISSettings.IconBasename = $Metadata.IconBasename } if ($Metadata.Namespace) { $NSISSettings.Namespace = $Metadata.Namespace } # $NSISSettings.DefaultInstallDir = "$($Metadata.Namespace)\$($NSISSettings.Name)" # } else { # $NSISSettings.DefaultInstallDir = "$($NSISSettings.Name)" # } # if we are in a prerelease : # - add '-preview' to name to differentiate 'preview' branches and 'release' branches # - concat Version and Build to get a 4-dotted version number for NSIS if ($Metadata.Prerelease) { # already done at the Get-Project level # $NSISSettings.name = "$($NSISSettings.name)-preview" # $NSISSettings.DisplayName = "$($NSISSettings.DisplayName) (preview)" $NSISSettings.Version = "$($Metadata.Version).$($Metadata.Build)" $NSISSettings.PreRelease = $Metadata.Prerelease } } return $NSISSettings } End { } } <# .SYNOPSIS Write a NSIS setup header file .DESCRIPTION Output a fully-formated NSIS setup header file based on build configuration. .PARAMETER Metadata The project's properties. Properties have to be filtered with ConvertTo-NSISSettings first .PARAMETER Destination Directory in which to put the resulting header file. The filename will be named header.nsi .PARAMETER PassThru Use this switch to output the content of the resulting file instead of its path .OUTPUTS Full path to header file .OUTPUTS header file content .EXAMPLE $project = gc ./project.yml | ConvertFrom-Yaml | ConvertTo-PSCustomObject $project | Out-NSISHeaderFile -Destination ./build/windows/ This example use a project.yml file filled with "key: pair" values, convert it to an object, an use its properties to output a well-formated NSIS header file. The output of this example is "./build/windows/header.nsi" #> function Out-NSISHeaderFile { [CmdletBinding()]Param ( [Parameter(Mandatory = $true,ValueFromPipeLine = $true)][hashtable]$Metadata, [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$Destination, [switch]$PassThru ) Begin { if (!(Test-DirExist $Destination)) { $null = New-Item -Path $Destination -ItemType Directory } $Filename = "$Destination/header.nsi" } Process { $NSISSettings = ConvertTo-NSISSettings -Metadata $Metadata "" | Set-Content $Filename -Encoding utf8 foreach ($k in $NSISSettings.Keys) { "!define $($k.ToUpper()) `"$($NSISSettings.$k)`"" | Out-File -FilePath $Filename -Encoding utf8 -Append } if ($PassThru) { return $NSISSettings } else { return (Resolve-Path -Path "$Filename").Path } } End { } } <# .SYNOPSIS Test if a project is viable to build .DESCRIPTION Before launching a build of your project, you can use this function to forsee if requirements are met. .PARAMETER Project The project definition object .EXAMPLE Get-Project | Test-NSISBuild .NOTES General notes #> function Test-NSISBuild { [CmdletBinding()][OutputType([String])]Param ( [Parameter(Mandatory = $true,ValueFromPipeLine = $true)][hashtable]$Project ) Begin { # Write-EnterFunction } Process { $rc = $true foreach ($f in @('LICENSE', "$($Project.Root)/images/favicon.ico")) { if (Test-Path $Path/$f -PathType Leaf) { Write-Host -ForegroundColor Green "[+] $((Resolve-Path $Path/$f).Path) exist" } else { Write-Host -ForegroundColor Red "[-] $Path/$f does not exist" $rc = $false } } return $rc } End { # Write-LeaveFunction } } <# .SYNOPSIS Build the project to a setup.exe .DESCRIPTION Build the project to a NullSoft Installer System setup.exe .EXAMPLE New-NSISBuild -Project (Get-Project) .NOTES The resulting setup will be named after project's data : `$name-$version-$arch.exe` if all data is available `$name-$version.exe` if $architecture is not available #> function New-NSISBuild { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low', DefaultParameterSetName = 'PROJECT')] [OutputType([Boolean], [String])] Param ( # The project object as returned by Get-Project [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][hashtable]$Project, # An optional header.nsi file to inject. If it is not used, a default one, based on $Project will be generated [Alias('Header')] [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$HeaderNSI, #An optional custom setup.nsi file [Alias('Configuration')] [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$SetupNSI, # An optional WINDOWS folder where windows package scripts are stored [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$WindowsFolder = './build/Windows', # The source folder of your project. Files and directory structure will be kept as-is [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$Source = "./src", #Destination folder to create resulting setup.exe [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$Destination = "./releases", # Override output package filename. # It defaults to projectName-Version-Arch.exe [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$OutputFileName, # Force/override/overwrite things [switch]$Force ) Begin { Write-EnterFunction if (!(Test-DirExist $Destination)) { $null = New-Item -Path $Destination -ItemType Directory } # Convert every path from \ char to / char to let regular expression work normally $WindowsFolder = $WindowsFolder -replace "\\", "/" $Source = $Source -replace "\\", "/" $Destination = $Destination -replace "\\", "/" $OutputFileName = $OutputFileName -replace "\\", "/" } Process { if (Test-DirExist $WindowsFolder) { Write-Debug "Copy Windows folder '$WindowsFolder' to '$Source/Windows'" # copy WINDOWS folder taking care of mustache template files Copy-Item $WindowsFolder -Recurse -Destination "$Source/Windows" -Force:$Force -Exclude "*.mustache" Get-ChildItem $WindowsFolder -Recurse -Filter "*.mustache" | ForEach-Object { $item = $_ # $destFilePath = $item.DirectoryName -replace "$WindowsFolder", "$Source/Windows" $destFileName = "$Source/Windows/$($item.Basename)" Write-Debug "Found mustache template file $($item.fullname)... convert it to '$destFilename'" ConvertFrom-MustacheTemplate -Template (Get-Content -Raw $item.fullname) -Values $Project | Out-File $destFileName } } # copy icon to proper location # Copy-Item "$($Project.Root)/$($Project.IconFile)" -Destination $Source -Force -Confirm:$false if (!$Project.IconFile) { $project.IconFile = (Resolve-Path "$($Script:PWSHFW_BUILDHELPERS_DIR)/Assets/application.ico").Path } if ($Project.IconFile) { $project.IconFile = ConvertTo-WindowsIcon -Image "$($Project.IconFile)" -Destination $Source } $Project.IconBasename = (Get-Item $project.IconFile).Name Write-Debug "Use Icon '$($project.IconFile)'" # Write-Host "Project = $Project" # Write-Host "HeaderNSI = $HeaderNSI" # Write-Host "SetupNSI = $SetupNSI" # Write-Host "PWSHFW_BUILDHELPERS_DIR = $PWSHFW_BUILDHELPERS_DIR" # Write-Host "PWSHFW_BUILDHELPERS_DIR = $Script:PWSHFW_BUILDHELPERS_DIR" # move to project's root Push-Location $Project.Root if (!($HeaderNSI)) { $HeaderNSI = Resolve-Path ($Project | Out-NSISHeaderFile -Destination $Project.Root) -Relative } if (!($SetupNSI)) { Copy-Item "$($Script:PWSHFW_BUILDHELPERS_DIR)/Assets/setup.nsi" -Destination $Project.Root $SetupNSI = Resolve-Path "$($Project.Root)/setup.nsi" -Relative } if ([string]::IsNullOrEmpty($OutputFileName)) { $Filename = "$($Project.Name)-$($Project.Version)$($Project.PreRelease)" if ($Project.Architecture) { $Filename += "-$($Project.Architecture)" } $Filename += ".exe" } else { $Filename = $OutputFileName } # clean filename from fancy characters # @see https://docs.microsoft.com/en-us/dotnet/standard/base-types/character-classes-in-regular-expressions#unicode-category-or-unicode-block-p # @see https://docs.microsoft.com/en-us/dotnet/standard/base-types/character-classes-in-regular-expressions#SupportedUnicodeGeneralCategories $Filename = $Filename -replace "[^\p{L}\p{Nd}-_.]" | Remove-StringLatinCharacters # discover where is nsis.exe if (fileExist($(${env:ProgramFiles(x86)} + "\NSIS\makensis.exe"))) { $MAKENSIS = "$(${env:ProgramFiles(x86)})\NSIS\makensis.exe" } if (fileExist($(${env:ProgramFiles} + "\NSIS\makensis.exe"))) { $MAKENSIS = "$($env:ProgramFiles)\NSIS\makensis.exe)" } if (!$MAKENSIS) { Write-Error "makensis.exe not found. Please install it first. Visit https://nsis.sourceforge.io/Main_Page" return $false } # compute debug level [uint16]$debugLevel = 0 if (($INFO) -or ($InformationPreference -eq 'Continue')) { $debugLevel = 1 } if (($VERBOSE) -or ($VerbosePreference -eq 'Continue')) { $debugLevel = 2 } if (($DEBUG) -or ($DebugPreference -eq 'Continue')) { $debugLevel = 3 } if (($DEVEL) -or ($DevelPreference -eq 'Continue')) { $debugLevel = 4 } if ($DEVEL) { Write-Enter "-- header.nsi ($HeaderNSI) ---------------------------" Get-Content $HeaderNSI | ForEach-Object { Write-Devel $_ } Write-Leave "-- header.nsi ---------------------------" Write-Enter "-- setup.nsi ($SetupNSI) ---------------------------" Get-Content $SetupNSI | ForEach-Object { Write-Devel $_ } Write-Leave "-- setup.nsi ---------------------------" } if ($PSCmdlet.ShouldProcess("$Destination/$Filename", "Create NSIS setup file")) { # it seems this command-line is too long... so we have to shorten it at the maximum # $rc = Execute-Command -exe "$MAKENSIS" "/V$debugLevel /NOCD /INPUTCHARSET UTF8 /OUTPUTCHARSET UTF8 /D'SOURCE=$Source' /D'ROOT=$($Project.Root)' '$HeaderNSI' /X'OutFile $Destination/$Filename' '$SetupNSI'" $rc = Execute-Command -exe "$MAKENSIS" "/V$debugLevel /NOCD /INPUTCHARSET UTF8 /OUTPUTCHARSET UTF8 /D'SOURCE=$Source' /D'ROOT=.' /D'USER_BUILD_WINDOWS_FOLDER=$WindowsFolder' '$HeaderNSI' /X'OutFile $Destination/$Filename' '$SetupNSI'" Write-Devel "rc = $rc" if (Test-FileExist "$Destination/$Filename") { $value = (Resolve-Path "$Destination/$Filename").Path } else { $value = $false } } else { $value = "$Destination/$Filename" } Pop-Location return $value } End { Write-LeaveFunction } } <# .SYNOPSIS Convert an image to a Windows icon .DESCRIPTION Convert an image to Windows icon `ico` file format. .PARAMETER Image Full path to an image file .PARAMETER Destination Destination folder .PARAMETER Filename Optional. New filename of the image .EXAMPLE ConvertTo-WindowsIco -Image /path/to/favicon.png .NOTES This function do not convert image size. It just convert format. #> function ConvertTo-WindowsIcon { [CmdletBinding()] [OutputType([string], [Boolean])] Param ( [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$Image, [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$Destination, [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$Filename ) Begin { Write-EnterFunction } Process { if (!(Test-FileExist $Image)) { eerror "Image '$Image' not found." return $null } $item = Get-Item $Image if ([string]::IsNullOrEmpty($Destination)) { $Destination = $item.DirectoryName } if ($item.Extension -eq '.ico') { $null = Copy-Item -Path "$($item.FullName)" -Destination "$Destination/$($item.name)" $rc = "$Destination/$($item.name)" Write-Debug "Icon found at '$rc'" } else { if ($Script:ConvertIsInstalled) { $basename = $item.BaseName # $ext = (Get-Item $Image).Extension if ([string]::IsNullOrEmpty($Filename)) { $Filename = $basename } $null = Execute-Command -exe "$Script:ConvertEXE" -args "$Image -define icon:auto-resize=256,128,64,48,32,16 $Destination/$Filename.ico" $rc = "$Destination/$Filename.ico" Write-Debug "Icon converted in '$rc'" } else { Write-Warning $Script:ConvertNotInstalledMessage $rc = $Image } } try { $rc = (Resolve-Path $rc).Path } catch { eerror "Path '$rc' not found." $rc = $null } return $rc } End { Write-LeaveFunction } } |