diff --git a/AU/Private/AUPackage.ps1 b/AU/Private/AUPackage.ps1 index c9c20ac4..5a67436d 100644 --- a/AU/Private/AUPackage.ps1 +++ b/AU/Private/AUPackage.ps1 @@ -12,6 +12,9 @@ class AUPackage { [bool] $Ignored [string] $IgnoreMessage + [string] $StreamsPath + [pscustomobject] $Streams + AUPackage([string] $Path ){ if ([String]::IsNullOrWhiteSpace( $Path )) { throw 'Package path can not be empty' } @@ -23,6 +26,9 @@ class AUPackage { $this.NuspecXml = [AUPackage]::LoadNuspecFile( $this.NuspecPath ) $this.NuspecVersion = $this.NuspecXml.package.metadata.version + + $this.StreamsPath = '{0}\{1}.json' -f $this.Path, $this.Name + $this.Streams = [AUPackage]::LoadStreams( $this.StreamsPath ) } static [xml] LoadNuspecFile( $NuspecPath ) { @@ -37,6 +43,25 @@ class AUPackage { [System.IO.File]::WriteAllText($this.NuspecPath, $this.NuspecXml.InnerXml, $Utf8NoBomEncoding) } + static [pscustomobject] LoadStreams( $StreamsPath ) { + if (!(Test-Path $StreamsPath)) { return $null } + $res = Get-Content $StreamsPath | ConvertFrom-Json + return $res + } + + UpdateStreams( [hashtable] $streams ){ + $streams.Keys | % { + $stream = $_.ToString() + $version = $streams[$_].Version.ToString() + if($this.Streams | Get-Member $stream) { + $this.Streams.$stream = $version + } else { + $this.Streams | Add-Member $stream $version + } + } + $this.Streams | ConvertTo-Json -Compress | Set-Content $this.StreamsPath -Encoding UTF8 + } + Backup() { $d = "$Env:TEMP\au\" + $this.Name diff --git a/AU/Private/AUVersion.ps1 b/AU/Private/AUVersion.ps1 new file mode 100644 index 00000000..e5d4ce86 --- /dev/null +++ b/AU/Private/AUVersion.ps1 @@ -0,0 +1,70 @@ +class AUVersion : System.IComparable { + [version] $Version + [string] $Prerelease + [string] $BuildMetadata + + AUVersion([version] $version, [string] $prerelease, [string] $buildMetadata) { + if (!$version) { throw 'Version cannot be null.' } + $this.Version = $version + $this.Prerelease = $prerelease + $this.BuildMetadata = $buildMetadata + } + + static [AUVersion] Parse([string] $input) { return [AUVersion]::Parse($input, $true) } + + static [AUVersion] Parse([string] $input, [bool] $strict) { + if (!$input) { throw 'Version cannot be null.' } + $reference = [ref] $null + if (![AUVersion]::TryParse($input, $reference, $strict)) { throw "Invalid version: $input." } + return $reference.Value + } + + static [bool] TryParse([string] $input, [ref] $result) { return [AUVersion]::TryParse($input, $result, $true) } + + static [bool] TryParse([string] $input, [ref] $result, [bool] $strict) { + $result.Value = [AUVersion] $null + if (!$input) { return $false } + $pattern = [AUVersion]::GetPattern($strict) + if ($input -notmatch $pattern) { return $false } + $reference = [ref] $null + if (![version]::TryParse($Matches['version'], $reference)) { return $false } + $result.Value = [AUVersion]::new($reference.Value, $Matches['prerelease'], $Matches['buildMetadata']) + return $true + } + + hidden static [string] GetPattern([bool] $strict) { + $versionPattern = '(?\d+(?:\.\d+){0,3})' + $identifierPattern = "[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*" + if ($strict) { + return "^$versionPattern(?:-(?$identifierPattern))?(?:\+(?$identifierPattern))?`$" + } else { + return "^v?\s*$versionPattern(?:-?(?$identifierPattern))?(?:\+(?$identifierPattern))?\s*`$" + } + } + + [int] CompareTo($obj) { + if ($obj -eq $null) { return 1 } + if ($obj -isnot [AUVersion]) { throw "AUVersion expected: $($obj.GetType())" } + if ($obj.Version -ne $this.Version) { return $this.Version.CompareTo($obj.Version) } + if ($obj.Prerelease -and $this.Prerelease) { return $this.Prerelease.CompareTo($obj.Prerelease) } + if (!$obj.Prerelease -and !$this.Prerelease) { return 0 } + if ($obj.Prerelease) { return 1 } + return -1 + } + + [bool] Equals($obj) { return $obj -is [AUVersion] -and $obj -and $this.ToString().Equals($obj.ToString()) } + + [int] GetHashCode() { return $this.ToString().GetHashCode() } + + [string] ToString() { + $result = $this.Version.ToString() + if ($this.Prerelease) { $result += "-$($this.Prerelease)" } + if ($this.BuildMetadata) { $result += "+$($this.BuildMetadata)" } + return $result + } + + [string] ToString([int] $fieldCount) { + if ($fieldCount -eq -1) { return $this.Version.ToString() } + return $this.Version.ToString($fieldCount) + } +} diff --git a/AU/Private/is_version.ps1 b/AU/Private/is_version.ps1 index 4e7db117..71d07577 100644 --- a/AU/Private/is_version.ps1 +++ b/AU/Private/is_version.ps1 @@ -1,8 +1,4 @@ # Returns [bool] function is_version( [string] $Version ) { - $re = '^(\d{1,16})\.(\d{1,16})\.*(\d{1,16})*\.*(\d{1,16})*(-[^.-]+)*$' - if ($Version -notmatch $re) { return $false } - - $v = $Version -replace '-.+' - return [version]::TryParse($v, [ref]($__)) + return [AUVersion]::TryParse($Version, [ref]($__)) } diff --git a/AU/Public/Get-Version.ps1 b/AU/Public/Get-Version.ps1 new file mode 100644 index 00000000..2e727695 --- /dev/null +++ b/AU/Public/Get-Version.ps1 @@ -0,0 +1,24 @@ +# Author: Thomas Démoulins + +<# +.SYNOPSIS + Get a semver-like object from a given version string. + +.DESCRIPTION + This function parses a string containing a semver-like version + and returns an object that represents both the version (with up to 4 parts) + and optionally a pre-release and a build metadata. + + The parsing is quite flexible: + - the string can starts with a 'v' + - there can be no hyphen between the version and the pre-release + - extra spaces (between any parts of the semver-like version) are ignored +#> +function Get-Version { + [CmdletBinding()] + param( + # Version string to parse. + [string] $Version + ) + return [AUVersion]::Parse($Version, $false) +} diff --git a/AU/Public/Push-Package.ps1 b/AU/Public/Push-Package.ps1 index 20ea16e7..dc05998b 100644 --- a/AU/Public/Push-Package.ps1 +++ b/AU/Public/Push-Package.ps1 @@ -3,22 +3,26 @@ <# .SYNOPSIS - Push latest created package to the Chocolatey community repository. + Push latest (or all) created package(s) to the Chocolatey community repository. .DESCRIPTION The function uses they API key from the file api_key in current or parent directory, environment variable or cached nuget API key. #> function Push-Package() { + param( + [switch] $All + ) $api_key = if (Test-Path api_key) { gc api_key } elseif (Test-Path ..\api_key) { gc ..\api_key } elseif ($Env:api_key) { $Env:api_key } - $package = ls *.nupkg | sort -Property CreationTime -Descending | select -First 1 - if (!$package) { throw 'There is no nupkg file in the directory'} + $packages = ls *.nupkg | sort -Property CreationTime -Descending + if (!$All) { $packages = $packages | select -First 1 } + if (!$packages) { throw 'There is no nupkg file in the directory'} if ($api_key) { - cpush $package.Name --api-key $api_key --source https://push.chocolatey.org + $packages | % { cpush $_.Name --api-key $api_key --source https://push.chocolatey.org } } else { - cpush $package.Name --source https://push.chocolatey.org + $packages | % { cpush $_.Name --source https://push.chocolatey.org } } } diff --git a/AU/Public/Update-AUPackages.ps1 b/AU/Public/Update-AUPackages.ps1 index c5a649bf..dcc7b82c 100644 --- a/AU/Public/Update-AUPackages.ps1 +++ b/AU/Public/Update-AUPackages.ps1 @@ -47,6 +47,7 @@ function Update-AUPackages { UpdateTimeout - Timeout for background job in seconds, by default 1200 (20 minutes). Force - Force package update even if no new version is found. Push - Set to true to push updated packages to Chocolatey community repository. + PushAll - Set to true to push all updated packages and not only the most recent one per folder. WhatIf - Set to true to set WhatIf option for all packages. PluginPath - Additional path to look for user plugins. If not set only module integrated plugins will work @@ -214,7 +215,7 @@ function Update-AUPackages { if ( "$type" -ne 'AUPackage') { throw "'$using:package_name' update script didn't return AUPackage but: $type" } if ($pkg.Updated -and $Options.Push) { - $pkg.Result += $r = Push-Package + $pkg.Result += $r = Push-Package -All:$Options.PushAll if ($LastExitCode -eq 0) { $pkg.Pushed = $true } else { diff --git a/AU/Public/Update-Package.ps1 b/AU/Public/Update-Package.ps1 index eacbce65..02ea75c4 100644 --- a/AU/Public/Update-Package.ps1 +++ b/AU/Public/Update-Package.ps1 @@ -82,6 +82,10 @@ function Update-Package { #Timeout for all web operations, by default 100 seconds. [int] $Timeout, + #Streams to process, either a string or an array. If ommitted, all streams are processed. + #Single stream required when Force is specified. + $Include, + #Force package update even if no new version is found. [switch] $Force, @@ -194,40 +198,109 @@ function Update-Package { invoke_installer } + function process_stream() { + if (!(is_version $package.NuspecVersion)) { + Write-Warning "Invalid nuspec file Version '$($package.NuspecVersion)' - using 0.0" + $global:Latest.NuspecVersion = $package.NuspecVersion = '0.0' + } + if (!(is_version $Latest.Version)) { throw "Invalid version: $($Latest.Version)" } + $package.RemoteVersion = $Latest.Version + + $Latest.NuspecVersion = [AUVersion] $Latest.NuspecVersion + $Latest.Version = [AUVersion] $Latest.Version + + if (!$NoCheckUrl) { check_urls } + + "nuspec version: " + $package.NuspecVersion | result + "remote version: " + $package.RemoteVersion | result + + $script:is_forced = $false + if ($Latest.Version -gt $Latest.NuspecVersion) { + if (!($NoCheckChocoVersion -or $Force)) { + $choco_url = "https://chocolatey.org/packages/{0}/{1}" -f $global:Latest.PackageName, $package.RemoteVersion + try { + request $choco_url $Timeout | out-null + "New version is available but it already exists in the Chocolatey community feed (disable using `$NoCheckChocoVersion`):`n $choco_url" | result + return + } catch { } + } + } else { + if (!$Force) { + 'No new version found' | result + return + } + else { 'No new version found, but update is forced' | result; set_fix_version } + } + + 'New version is available' | result + + $match_url = ($Latest.Keys | ? { $_ -match '^URL*' } | select -First 1 | % { $Latest[$_] } | split-Path -Leaf) -match '(?<=\.)[^.]+$' + if ($match_url -and !$Latest.FileType) { $Latest.FileType = $Matches[0] } + + if ($ChecksumFor -ne 'none') { get_checksum } else { 'Automatic checksum skipped' | result } + + if ($WhatIf) { $package.Backup() } + try { + if (Test-Path Function:\au_BeforeUpdate) { 'Running au_BeforeUpdate' | result; au_BeforeUpdate $package | result } + if (!$NoReadme -and (Test-Path "$($package.Path)\README.md")) { Set-DescriptionFromReadme $package -SkipFirst 2 | result } + update_files + if (Test-Path Function:\au_AfterUpdate) { 'Running au_AfterUpdate' | result; au_AfterUpdate $package | result } + + choco pack --limit-output | result + if ($LastExitCode -ne 0) { throw "Choco pack failed with exit code $LastExitCode" } + } finally { + if ($WhatIf) { + $save_dir = $package.SaveAndRestore() + Write-Warning "Package restored and updates saved to: $save_dir" + } + } + + $package.Updated = $true + } + function set_fix_version() { $script:is_forced = $true if ($global:au_Version) { "Overriding version to: $global:au_Version" | result - $global:Latest.Version = $package.RemoteVersion = $global:au_Version - if (!(is_version $Latest.Version)) { throw "Invalid version: $($Latest.Version)" } + $package.RemoteVersion = $global:au_Version + if (!(is_version $global:au_Version)) { throw "Invalid version: $global:au_Version" } + $global:Latest.Version = [AUVersion] $package.RemoteVersion $global:au_Version = $null return } $date_format = 'yyyyMMdd' $d = (get-date).ToString($date_format) - $v = [version]($package.NuspecVersion -replace '-.+') + $v = $Latest.NuspecVersion.Version $rev = $v.Revision.ToString() try { $revdate = [DateTime]::ParseExact($rev, $date_format,[System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None) } catch {} if (($rev -ne -1) -and !$revdate) { return } $build = if ($v.Build -eq -1) {0} else {$v.Build} - $Latest.Version = $package.RemoteVersion = '{0}.{1}.{2}.{3}' -f $v.Major, $v.Minor, $build, $d + $package.RemoteVersion = '{0}.{1}.{2}.{3}' -f $v.Major, $v.Minor, $build, $d + $Latest.Version = [AUVersion] $package.RemoteVersion + } + + function set_latest( [HashTable]$latest, [string] $version ) { + if (!$latest.PackageName) { $latest.PackageName = $package.Name } + if (!$latest.NuspecVersion) { $latest.NuspecVersion = $version } + $package.NuspecVersion = $latest.NuspecVersion + $global:Latest = $latest } function update_files( [switch]$SkipNuspecFile ) { 'Updating files' | result - ' $Latest data:' | result; ($global:Latest.keys | sort | % { " {0,-15} ({1}) {2}" -f $_, $global:Latest[$_].GetType().Name, $global:Latest[$_] }) | result; '' | result + ' $Latest data:' | result; ($global:Latest.keys | sort | % { " {0,-25} {1,-12} {2}" -f $_, "($($global:Latest[$_].GetType().Name))", $global:Latest[$_] }) | result if (!$SkipNuspecFile) { " $(Split-Path $package.NuspecPath -Leaf)" | result - " setting id: $($global:Latest.PackageName)" | result + " setting id: $($global:Latest.PackageName)" | result $package.NuspecXml.package.metadata.id = $package.Name = $global:Latest.PackageName.ToString() - $msg ="updating version: {0} -> {1}" -f $package.NuspecVersion, $package.RemoteVersion + $msg = " updating version: {0} -> {1}" -f $package.NuspecVersion, $package.RemoteVersion if ($script:is_forced) { if ($package.RemoteVersion -eq $package.NuspecVersion) { $msg = " version not changed as it already uses 'revision': {0}" -f $package.NuspecVersion @@ -250,7 +323,7 @@ function Update-Package { # is detected as ANSI $fileContent = gc $fileName -Encoding UTF8 $sr[ $fileName ].GetEnumerator() | % { - (' {0} = {1} ' -f $_.name, $_.value) | result + (' {0,-35} = {1}' -f $_.name, $_.value) | result if (!($fileContent -match $_.name)) { throw "Search pattern not found: '$($_.name)'" } $fileContent = $fileContent -replace $_.name, $_.value } @@ -262,20 +335,6 @@ function Update-Package { } } - function is_updated() { - $remote_l = $package.RemoteVersion -replace '-.+' - $nuspec_l = $package.NuspecVersion -replace '-.+' - $remote_r = $package.RemoteVersion.Replace($remote_l,'') - $nuspec_r = $package.NuspecVersion.Replace($nuspec_l,'') - - if ([version]$remote_l -eq [version] $nuspec_l) { - if (!$remote_r -and $nuspec_r) { return $true } - if ($remote_r -and !$nuspec_r) { return $false } - return ($remote_r -gt $nuspec_r) - } - [version]$remote_l -gt [version] $nuspec_l - } - function result() { if ($global:Silent) { return } @@ -305,13 +364,6 @@ function Update-Package { $package = [AUPackage]::new( $pwd ) if ($Result) { sv -Scope Global -Name $Result -Value $package } - $global:Latest = @{PackageName = $package.Name} - $global:Latest.NuspecVersion = $package.NuspecVersion - if (!(is_version $package.NuspecVersion)) { - Write-Warning "Invalid nuspec file Version '$($package.NuspecVersion)' - using 0.0" - $global:Latest.NuspecVersion = $package.NuspecVersion = '0.0' - } - [System.Net.ServicePointManager]::SecurityProtocol = 'Ssl3,Tls,Tls11,Tls12' #https://github.com/chocolatey/chocolatey-coreteampackages/issues/366 $module = $MyInvocation.MyCommand.ScriptBlock.Module "{0} - checking updates using {1} version {2}" -f $package.Name, $module.Name, $module.Version | result @@ -324,63 +376,50 @@ function Update-Package { $res_type = $res.GetType() if ($res_type -ne [HashTable]) { throw "au_GetLatest doesn't return a HashTable result but $res_type" } - $res.Keys | % { $global:Latest.Remove($_) } - $global:Latest += $res - if ($global:au_Force) { $Force = $true } + if ($global:au_Force) { + $Force = $true + if ($global:au_Include) { $Include = $global:au_Include } + } } catch { throw "au_GetLatest failed`n$_" } - if (!(is_version $Latest.Version)) { throw "Invalid version: $($Latest.Version)" } - $package.RemoteVersion = $Latest.Version - - if (!$NoCheckUrl) { check_urls } - - "nuspec version: " + $package.NuspecVersion | result - "remote version: " + $package.RemoteVersion | result + if ($res.Streams) { + if ($res.Streams -isnot [HashTable]) { throw "au_GetLatest's streams don't return a HashTable result but $($res.Streams.GetType())" } - if (is_updated) { - if (!($NoCheckChocoVersion -or $Force)) { - $choco_url = "https://chocolatey.org/packages/{0}/{1}" -f $global:Latest.PackageName, $package.RemoteVersion - try { - request $choco_url $Timeout | out-null - "New version is available but it already exists in the Chocolatey community feed (disable using `$NoCheckChocoVersion`):`n $choco_url" | result - return $package - } catch { } + if ($Include) { + if ($Include -isnot [string] -and $Include -isnot [Array]) { throw "`$Include must be either a String or an Array but is $($Include.GetType())" } + if ($Include -is [string]) { [Array] $Include = $Include -split ',' | foreach { ,$_.Trim() } } } - } else { - if (!$Force) { - 'No new version found' | result - return $package - } - else { 'No new version found, but update is forced' | result; set_fix_version } - } + if ($Force -and (!$Include -or $Include.Length -ne 1)) { throw 'A single stream must be included when forcing package update' } - 'New version is available' | result - - $match_url = ($Latest.Keys | ? { $_ -match '^URL*' } | select -First 1 | % { $Latest[$_] } | split-Path -Leaf) -match '(?<=\.)[^.]+$' - if ($match_url -and !$Latest.FileType) { $Latest.FileType = $Matches[0] } + if ($Include) { + $streams = @{} + $res.Streams.Keys | ? { $_ -in $Include } | % { + $streams.Add($_, $res.Streams[$_]) + } + } else { + $streams = $res.Streams + } - if ($ChecksumFor -ne 'none') { get_checksum } else { 'Automatic checksum skipped' | result } + $streams.Keys | ? { !$Include -or $_ -in $Include } | sort { [AUVersion]$_ } | % { + $stream = $streams[$_] + if ($stream -isnot [HashTable]) { throw "au_GetLatest's $_ stream doesn't return a HashTable result but $($stream.GetType())" } - if ($WhatIf) { $package.Backup() } - try { - if (Test-Path Function:\au_BeforeUpdate) { 'Running au_BeforeUpdate' | result; au_BeforeUpdate $package | result } - if (!$NoReadme -and (Test-Path "$($package.Path)\README.md")) { Set-DescriptionFromReadme $package -SkipFirst 2 | result } - update_files - if (Test-Path Function:\au_AfterUpdate) { 'Running au_AfterUpdate' | result; au_AfterUpdate $package | result } - - choco pack --limit-output | result - if ($LastExitCode -ne 0) { throw "Choco pack failed with exit code $LastExitCode" } - } finally { - if ($WhatIf) { - $save_dir = $package.SaveAndRestore() - Write-Warning "Package restored and updates saved to: $save_dir" + '' | result + "*** Stream: $_ ***" | result + set_latest $stream $package.Streams.$_ + process_stream } + + $package.UpdateStreams($streams) + } else { + '' | result + set_latest $res $package.NuspecVersion + process_stream } - 'Package updated' | result - $package.Updated = $true + if ($package.Updated) { 'Package updated' | result } return $package } diff --git a/README.md b/README.md index 1cc26c48..a5392af7 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ To see AU in action see [video tutorial](https://www.youtube.com/watch?v=m2XpV2L ## Features - Use only PowerShell to create automatic update script for given package. +- Handles multiple streams with a single update script. - Automatically downloads installers and provides/verifies checksums for x32 and x64 versions. - Verifies URLs, nuspec versions, remote repository existence etc. - Automatically sets the Nuspec descriptions from a README.md files. @@ -62,6 +63,32 @@ function global:au_GetLatest { The returned version is later compared to the one in the nuspec file and if remote version is higher, the files will be updated. The returned keys of this HashTable are available via global variable `$global:Latest` (along with some keys that AU generates). You can put whatever data you need in the returned HashTable - this data can be used later in `au_SearchReplace`. +When multiple streams have to be handled, multiple HashTables can be put together in order to describe each supported stream. + +```powershell +function global:au_GetLatest { + # ... + $streams = @{} + $streams.Add('1.2', @{ Version = $version12; URL32 = $url12 }) + $streams.Add('1.3', @{ Version = $version13; URL32 = $url13 }) + return @{ Streams = $streams } +} +``` + +In order to help working with versions, function `Get-Version` can be called in order to parse [semver](http://semver.org/) versions in a flexible manner. It returns an `AUVersion` object with all the details about the version. Furthermore, this object can be compared and sorted. + +``` +PS C:\> Get-Version 'v1.3.2.7rc1' + +Version Prerelease BuildMetadata +------- ---------- ------------- +1.3.2.7 rc1 + + +PS C:\> $version = Get-Version '1.3.2-beta2+5' +PS C:\> $version.ToString(2) + ' => ' + $version.ToString() +1.3 => 1.3.2-beta2+5 +``` ### `au_SearchReplace`