diff --git a/.devcontainer.json b/.devcontainer.json index 396c9ba5de..0c89314809 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -11,23 +11,27 @@ "profile": "default" } }, - "extensions": [ - "DavidAnson.vscode-markdownlint", - "eamodio.gitlens", - "EditorConfig.EditorConfig", - "gruntfuggly.todo-tree", - "k--kato.intellij-idea-keybindings", - "medo64.render-crlf", - "ms-vscode.powershell-preview", - "redhat.vscode-yaml", - "usernamehw.errorlens", - "yzhang.markdown-all-in-one" - ], - "settings": { - "terminal.integrated.defaultProfile.linux": "zsh", - "powershell.powerShellDefaultVersion": "PowerShell Core (Linux)", - "powershell.powerShellAdditionalExePaths": { - "PowerShell Core (Linux)": "/usr/local/bin/pwsh" + "customizations": { + "vscode": { + "extensions": [ + "DavidAnson.vscode-markdownlint", + "eamodio.gitlens", + "EditorConfig.EditorConfig", + "gruntfuggly.todo-tree", + "k--kato.intellij-idea-keybindings", + "medo64.render-crlf", + "ms-vscode.powershell-preview", + "redhat.vscode-yaml", + "usernamehw.errorlens", + "yzhang.markdown-all-in-one" + ], + "settings": { + "terminal.integrated.defaultProfile.linux": "zsh", + "powershell.powerShellDefaultVersion": "PowerShell Core (Linux)", + "powershell.powerShellAdditionalExePaths": { + "PowerShell Core (Linux)": "/usr/local/bin/pwsh" + } + } } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d9e0ce9b..63eaa18e7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [0.6.5](https://github.com/Ash258/Scoop-Core/milestone/5) +- **Autoupdate**: Properly support multiple items for specific properties - **Git**: Call git internally to prevent spawning of `cmd.exe` or `$SHELL` - **getopt**: Support multiple same options passed - **Config**: Prefer `~/.config/Shovel` over `~/.config/scoop` @@ -23,7 +24,7 @@ - **scoop-info**: Adopt new resolve function for parameter passing - **Diagnostic**: Ignore completion check when `-noprofile` is used (cmd usage most likely) - **commands**: Short option `-a` will not produce default architecture always -- Increase command startup +- Improve command startup - Prevent MSI installations in NanoServer images - Installation will not fail if `commonstartmenu` or `startmenu` system folder is not defined - Print not supported message only if operation will be executed diff --git a/lib/Helpers.ps1 b/lib/Helpers.ps1 index 6205f609fc..7d30d13fd1 100644 --- a/lib/Helpers.ps1 +++ b/lib/Helpers.ps1 @@ -248,7 +248,7 @@ function Invoke-VariableSubstitution { } } 'Object[]' { - $newEntity = $newEntity | ForEach-Object { Invoke-VariableSubstitution -Entity $_ -Substitutes $Substitutes -EscapeRegularExpression:$regexEscape } + $newEntity = $newEntity | ForEach-Object { , (Invoke-VariableSubstitution -Entity $_ -Substitutes $Substitutes -EscapeRegularExpression:$regexEscape) } } 'PSCustomObject' { $newentity.PSObject.Properties | ForEach-Object { $_.Value = Invoke-VariableSubstitution -Entity $_ -Substitutes $Substitutes -EscapeRegularExpression:$regexEscape } diff --git a/lib/autoupdate.ps1 b/lib/autoupdate.ps1 index 834ab812fa..a3976346e1 100644 --- a/lib/autoupdate.ps1 +++ b/lib/autoupdate.ps1 @@ -309,46 +309,6 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u return $hash } -function update_manifest_with_new_version($json, [String] $version, [String] $url, [String] $hash, $architecture = $null) { - $json.version = $version - - if ($null -eq $architecture) { - if ($json.url -is [System.Array]) { - $json.url[0] = $url - $json.hash[0] = $hash - } else { - $json.url = $url - $json.hash = $hash - } - } else { - # If there are multiple urls we replace the first one - if ($json.architecture.$architecture.url -is [System.Array]) { - $json.architecture.$architecture.url[0] = $url - $json.architecture.$architecture.hash[0] = $hash - } else { - $json.architecture.$architecture.url = $url - $json.architecture.$architecture.hash = $hash - } - } -} - -function update_manifest_prop([String] $prop, $json, [Hashtable] $substitutions) { - # first try the global property - if ($json.$prop -and $json.autoupdate.$prop) { - $json.$prop = Invoke-VariableSubstitution -Entity $json.autoupdate.$prop -Substitutes $substitutions - } - - # check if there are architecture specific variants - if ($json.architecture -and $json.autoupdate.architecture) { - $json.architecture | Get-Member -MemberType 'NoteProperty' | ForEach-Object { - $architecture = $_.Name - if ($json.architecture.$architecture.$prop -and $json.autoupdate.architecture.$architecture.$prop) { - $json.architecture.$architecture.$prop = Invoke-VariableSubstitution -Entity (arch_specific $prop $json.autoupdate $architecture) -Substitutes $substitutions - } - } - } -} - function Get-VersionSubstitution ([String] $Version, [Hashtable] $CustomMatches = @{ }) { $firstPart = $Version -split '-' | Select-Object -First 1 $lastPart = $Version -split '-' | Select-Object -Last 1 @@ -379,76 +339,124 @@ function Get-VersionSubstitution ([String] $Version, [Hashtable] $CustomMatches return $versionVariables } -function Invoke-Autoupdate ([String] $app, $dir, $json, [String] $version, [Hashtable] $MatchesHashtable, [String] $Extension = '.json', [Switch] $IgnoreArchive) { - Write-UserMessage -Message "Autoupdating $app" -Color 'DarkCyan' - - $oldVersion = $json.version - $oldJson = $json | ConvertTo-Json -Depth 50 | ConvertFrom-Json # Deep clone object - $has_changes = $false - $has_errors = $false - [bool] $valid = $true - $substitutions = Get-VersionSubstitution -Version $version -CustomMatches $MatchesHashtable +function Update-ManifestProperty { + [CmdletBinding()] + [OutputType([Boolean])] + param ( + [Parameter(Mandatory)] + [Alias('InputObject')] + $Manifest, + [Parameter(ValueFromPipeline)] + [String[]] $Property, + [String] $AppName, + [String] $Version, + [HashTable] $Substitutions + ) + + begin { $manifestChanged = $false } + + process { + $AppName | Out-Null # PowerShell/PSScriptAnalyzer#1472 + + foreach ($prop in $Property) { + if ($prop -eq 'hash') { + if ($Manifest.hash) { + # Substitute new value + $newURL = Invoke-VariableSubstitution -Entity $Manifest.autoupdate.url -Parameters $Substitutions + $newHash = _getHashesForUrls -AppName $AppName -Version $Version -HashExtraction $Manifest.autoupdate.hash -URL $newURL -Substitutions $Substitutions + + # Update manifest + $Manifest.hash, $propertyChanged = _updateSpecificProperty -Property $Manifest.hash -Value $newHash + $manifestChanged = $manifestChanged -or $propertyChanged + } else { + # Arch-spec + $Manifest.architecture | Get-Member -MemberType NoteProperty | ForEach-Object { + $arch = $_.Name + # Substitute new URLS + $newURL = Invoke-VariableSubstitution -Entity (arch_specific 'url' $Manifest.autoupdate $arch) -Parameters $Substitutions + # Calculate/extract hashes + $newHash = _getHashesForUrls -AppName $AppName -Version $Version -HashExtraction (arch_specific 'hash' $Manifest.autoupdate $arch) -URL $newURL -Substitutions $Substitutions + + # Update manifest + $Manifest.architecture.$arch.hash, $propertyChanged = _updateSpecificProperty -Property $Manifest.architecture.$arch.hash -Value $newHash + $manifestChanged = $manifestChanged -or $propertyChanged + } + } + # Extract and update hash property + } elseif ($Manifest.$prop -and $Manifest.autoupdate.$Prop) { + # Substitute new value + $autoupdateProperty = $Manifest.autoupdate.$prop + $newValue = Invoke-VariableSubstitution -Entity $autoupdateProperty -Substitutes $Substitutions + + if (($autoupdateProperty.GetType().Name -eq 'Object[]') -and ($autoupdateProperty.Length -eq 1)) { + # Make sure it's an array + $newValue = , $newValue + } - if ($json.url) { - # Create new url - $url = Invoke-VariableSubstitution -Entity $json.autoupdate.url -Substitutes $substitutions - $valid = $true - - if ($valid) { - # Create hash - $hash = get_hash_for_app $app $json.autoupdate.hash $version $url $substitutions - if ($null -eq $hash) { - $valid = $false - Write-UserMessage -Message 'Could not find hash or download URL!' -Color 'DarkRed' + # Update manifest + $Manifest.$prop, $propertyChanged = _updateSpecificProperty -Property $Manifest.$prop -Value $newValue + $manifestChanged = $manifestChanged -or $propertyChanged + } elseif ($Manifest.architecture) { + # Substitute and update architecture specific property + $Manifest.architecture | Get-Member -MemberType NoteProperty | ForEach-Object { + $arch = $_.Name + if ($Manifest.architecture.$arch.$prop -and ($Manifest.autoupdate.architecture.$arch.$prop -or $Manifest.autoupdate.$prop)) { + # Substitute new value + $autoupdateProperty = @(arch_specific $prop $Manifest.autoupdate $arch) + + $newValue = Invoke-VariableSubstitution -Entity $autoupdateProperty -Substitutes $Substitutions + if (($autoupdateProperty.GetType().Name -eq 'Object[]') -and ($autoupdateProperty.Length -eq 1)) { + # Make sure it's an array + $newValue = , $newValue + } + + # Update manifest + $Manifest.architecture.$arch.$prop, $propertyChanged = _updateSpecificProperty -Property $Manifest.architecture.$arch.$prop -Value $newValue + $hasManifestChanged = $hasManifestChanged -or $propertyChanged + } + } } } + } - # Write changes to the json object - if ($valid) { - $has_changes = $true - update_manifest_with_new_version $json $version $url $hash - } else { - $has_errors = $true - throw "Could not update $app" + end { + if (($Version -ne '') -and ($Manifest.version -ne $Version)) { + $Manifest.version = $Version + $manifestChanged = $true } - } else { - $json.architecture | Get-Member -MemberType 'NoteProperty' | ForEach-Object { - $valid = $true - $architecture = $_.Name - - # Create new url - $url = Invoke-VariableSubstitution -Entity (arch_specific 'url' $json.autoupdate $architecture) -Substitutes $substitutions - $valid = $true - - if ($valid) { - # Create hash - $hash = get_hash_for_app $app (arch_specific 'hash' $json.autoupdate $architecture) $version $url $substitutions - if ($null -eq $hash) { - $valid = $false - Write-UserMessage -Message 'Could not find hash or download URL!' -Color 'DarkRed' - } - } - # Write changes to the json object - if ($valid) { - $has_changes = $true - update_manifest_with_new_version $json $version $url $hash $architecture - } else { - $has_errors = $true - throw "Could not update $app $architecture" - } + return $manifestChanged + } +} + +function Invoke-Autoupdate ([String] $app, $dir, $json, [String] $version, [Hashtable] $MatchesHashtable, [String] $Extension = '.json', [Switch] $IgnoreArchive) { + Write-Host "Autoupdating $app" -ForegroundColor DarkCyan + + # For archive purpose + $oldVersion = $json.version + $oldJson = $json | ConvertTo-Json -Depth 50 | ConvertFrom-Json # Deep clone object + + $Manifest = $json + $substitutions = Get-VersionSubstitution -Version $version -CustomMatches $MatchesHashtable + + # Get properties, which needs to be updated + $updatedProperties = @(@($Manifest.autoupdate.PSObject.Properties.Name) -ne 'architecture') + if ($Manifest.autoupdate.architecture) { + $Manifest.autoupdate.architecture.PSObject.Properties | ForEach-Object { + $updatedProperties += $_.Value.PSObject.Properties.Name } } - # Update properties - update_manifest_prop 'extract_dir' $json $substitutions - update_manifest_prop 'changelog' $json $substitutions + # Hashes needs to be updated if not explicitly specified + if ($updatedProperties -contains 'url') { $updatedProperties += 'hash' } - # Update license - update_manifest_prop 'license' $json $substitutions + $updatedProperties = $updatedProperties | Select-Object -Unique + debug [$updatedProperties] + + $changed = Update-ManifestProperty -Manifest $Manifest -Property $updatedProperties -AppName $app -Version $version -Substitutions $substitutions $newManifest = $null - if ($has_changes -and !$has_errors) { + if ($changed) { # Archive older version if (!$IgnoreArchive -and ($json.autoupdate.archive -and ($json.autoupdate.archive -eq $true))) { $appOldPath = Join-Path $dir "old\$app" @@ -474,4 +482,110 @@ function Invoke-Autoupdate ([String] $app, $dir, $json, [String] $version, [Hash return $newManifest } -$__importedAutoupdate__ = $true +#region Helpers +function _updateSpecificProperty { + <# + .SYNOPSIS + Helper for updating manifest's property + .DESCRIPTION + Updates manifest property (String, Array or PSCustomObject). + .PARAMETER Property + Specifies the name of property to be updated. + .PARAMETER Value + Specifies the new value of property. + Update line by line. + .OUTPUTS + System.Object[] + The first element is new value of property, the second element is change flag + #> + param ( + [ValidateNotNull()] + [Parameter(Mandatory, ValueFromPipeline)] + [Object] $Property, + [Parameter(Mandatory)] + [Object] $Value + ) + begin { + $result = $Property + $hasChanged = $false + } + + process { + # Bind value into property in case the new value is "longer" than current. + if (@($Property).Length -lt @($Value).Length) { + $result = $Value + $hasChanged = $true + } else { + switch ($Property.GetType().Name) { + 'String' { + $val = $Value -as [String] + if ($null -ne $val) { + $result = $val + $hasChanged = $true + } + } + 'Object[]' { + $val = @($Value) + for ($i = 0; $i -lt $val.Length; $i++) { + $result[$i], $itemChanged = _updateSpecificProperty -Property $Property[$i] -Value $val[$i] + $hasChanged = $hasChanged -or $itemChanged + } + } + 'PSCustomObject' { + if ($Value -is [PSObject]) { + foreach ($name in $Property.PSObject.Properties.Name) { + if ($Value.$name) { + $result.$name, $itemChanged = _updateSpecificProperty -Property $Property.$name -Value $Value.$name + $hasChanged = $hasChanged -or $itemChanged + } + } + } + } + } + } + } + + end { return $result, $hasChanged } +} + +function _getHashesForUrls { + <# + .SYNOPSIS + Helper for extracting hashes. + .DESCRIPTION + Extract or calculate hash(es) for provided URLs. + If number of hash extraction templates is less then URLs, the last template will be reused for the rest URLs. + .PARAMETER AppName + Specifies the name of the application. + .PARAMETER Version + Specifies the version of the application. + .PARAMETER HashExtraction + Specifeis the extraction method. + .PARAMETER URL + Specifes the links to updated files. + .PARAMETER Substitutions + Specifes the hashtable with substitutions. + #> + param ( + [String] $AppName, + [String] $Version, + [PSObject[]] $HashExtraction, + [String[]] $URL, + [HashTable] $Substitutions + ) + $hash = @() + for ($i = 0; $i -lt $URL.Length; $i++) { + if ($null -eq $HashExtraction) { + $extract = $null + } else { + $extract = $HashExtraction[$i], $HashExtraction[-1] | Select-Object -First 1 + } + $hash += get_hash_for_app $AppName $extract $Version $URL[$i] $Substitutions + if ($null -eq $hash[$i]) { + throw "Could not update $AppName, hash for $(url_remote_filename $URL[$i]) failed!" + } + } + + return $hash +} +#endregion Helpers diff --git a/lib/install.ps1 b/lib/install.ps1 index f8c017eeda..1c96840582 100644 --- a/lib/install.ps1 +++ b/lib/install.ps1 @@ -683,7 +683,7 @@ function cookie_header($cookies) { } function is_in_dir($dir, $check) { - $check -match "^$([System.Text.RegularExpressions.Regex]::Escape("$dir"))(\\|`$)" + return $check -match "^$([System.Text.RegularExpressions.Regex]::Escape("$dir"))([\\/]|`$)" } function ftp_file_size($url) {