diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml new file mode 100644 index 000000000..31a3f928e --- /dev/null +++ b/.github/workflows/installer.yml @@ -0,0 +1,114 @@ +name: installer + +on: + push: + paths: + - build/* + - .github/* + pull_request: + paths: + - build/* + - .github/* + release: +defaults: + run: + shell: pwsh +env: + SCRIPT_URL: "https://raw.githubusercontent.com/QutEcoacoustics/audio-analysis/${{ github.sha }}/build/download_ap.ps1" +jobs: + test-script-host: + runs-on: ubuntu-latest + steps: + - name: ensure script host serves the script with "charset=utf-8" + run: | + curl -Ii "$env:SCRIPT_URL" | Write-Output -OutVariable "response" + $response | grep "charset=utf-8" + - name: ensure short url redirects to download script + # if this test ever fails, recreate short url and update the installing.md docs + run: | + curl -Ii "https://git.io/JtOo3" | Write-Output -OutVariable "response" + $response | grep "https://raw.githubusercontent.com/QutEcoacoustics/audio-analysis/.*/build/download_ap.ps1" + + test-installer: + strategy: + fail-fast: false + matrix: + os: + - windows-latest + - ubuntu-latest + - macos-latest + include: + - os: windows-latest + alias_name: "AP.exe" + bin_dir: "~\\AP" + - os: ubuntu-latest + alias_name: "AP" + bin_dir: "~/.local/bin" + - os: macos-latest + alias_name: "AP" + bin_dir: "~/.local/bin" + # https://raw.githubusercontent.com/QutEcoacoustics/audio-analysis/5f08faa96d8f044fb278b68f1c7d577fe218e8a1/build/download_ap.ps1 + runs-on: ${{ matrix.os }} + name: Test installer (${{ matrix.os }}) + # This workflow tests if our "installer" + # script works. + # It needs to test: + # - remote download + # - install + # - adding to PATH + # - uninstall + # Need to use -Force on commands because CI is not interactive + # https://github.com/PowerShell/PowerShell/issues/3337 + steps: + - name: Get info about action runner + run: | + $PSVersionTable + - name: Test installer (remote download) + uses: knicknic/os-specific-run@v1.0.3 + with: + macos: > + echo '$function:i=irm "$env:SCRIPT_URL";i -Force' | pwsh -nop -c - + linux: > + echo '$function:i=irm "$env:SCRIPT_URL";i -Force' | pwsh -nop -c - + windows: > + '$function:i=irm "$env:SCRIPT_URL";i -Force' | pwsh -nop -ex B -c - + - name: Test installer (upgrade) + uses: knicknic/os-specific-run@v1.0.3 + with: + macos: > + echo '$function:i=irm "$env:SCRIPT_URL";i -Force -Prerelease' | pwsh -nop -c - + linux: > + echo '$function:i=irm "$env:SCRIPT_URL";i -Force -Prerelease' | pwsh -nop -c - + windows: > + '$function:i=irm "$env:SCRIPT_URL";i -Force -Prerelease' | pwsh -nop -ex B -c - + - name: Update PATH + # Github actions doesn't persist changes to PATH during steps + # https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#adding-a-system-path + run: > + echo "$env:AP_PATH" | Resolve-Path | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + env: + AP_PATH: ${{ matrix.bin_dir }} + - name: Check alias works + run: | + Get-ChildItem "$env:AP_PATH/$env:ALIAS_NAME" + AP --version + env: + AP_PATH: ${{ matrix.bin_dir }} + ALIAS_NAME: ${{ matrix.alias_name }} + - name: Test installer (uninstall) + uses: knicknic/os-specific-run@v1.0.3 + with: + macos: > + echo '$function:i=irm "$env:SCRIPT_URL";i -Force -Uninstall' | pwsh -nop -c - + linux: > + echo '$function:i=irm "$env:SCRIPT_URL";i -Force -Uninstall' | pwsh -nop -c - + windows: > + '$function:i=irm "$env:SCRIPT_URL";i -Force -Uninstall' | pwsh -nop -ex B -c - + - name: Check alias no longer works + run: | + Get-Command AP -ErrorAction 'Continue' + if ($error[0] -match "The term 'AP' is not recognized") { + exit 0 + } + exit 1 + diff --git a/.vscode/settings.json b/.vscode/settings.json index c306443b0..a1902edbb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "Towsey", "Truskinger", "nyquist", + "pwsh", "rects" ] } \ No newline at end of file diff --git a/build/download_ap.ps1 b/build/download_ap.ps1 index 947900fb8..9e2bf1234 100755 --- a/build/download_ap.ps1 +++ b/build/download_ap.ps1 @@ -1,283 +1,653 @@ -#!/usr/bin/pwsh - -#Requires -Version 6 - -# .SYNOPSIS -# Downloads and "installs" (or updates) AP.exe -# .DESCRIPTION -# Downloads and installs AnalysisPrograms.exe. It can retieve releases from GitHub or AppVeyor. -# It defaults to installing in the root drive. -# For Windows machines it will add the install directory to the PATH envivronment variable. -# For Linux and MacOS it will symlink the binary into /usr/bin. -# AP.exe does not automatically update itself and currently is not integrated with any system package managers. -# .PARAMETER package -# Downloads one of the pre-defined versions, either Stable, Weekly, or Continuous -# .PARAMETER version -# The AP.exe version to download from GitHub. Must be a specific version (e.g. "18.8.1.2"). -# .PARAMETER ci_build_number -# The AP.exe build to download from AppVeyor. Must be the CI build number (e.g. "314"). -# .PARAMETER destination -# Where to "install" AP.exe to. Defaults to "/AP" ("C:\AP" on Windows) -# .PARAMETER github_api_token -# Provide this token to avoid rate limiting by the GitHub API. -# .EXAMPLE -# C:\> ./download_ap.ps1 -# .EXAMPLE -# C:\> ./download_ap.ps1 -package Stable -# .EXAMPLE -# C:\> ./download_ap.ps1 -package Weekly -# .EXAMPLE -# C:\> ./download_ap.ps1 -package Continuous -# .LINK -# https://github.com/QutEcoacoustics/audio-analysis/blob/master/docs/installing.md +#!/usr/bin/pwsh -l + +#Requires -Version 7 + +<# +.SYNOPSIS +Downloads and "installs" (or updates), checks the installation, or uninstalls AP.exe +.DESCRIPTION +This script will install, check or uninstall QUT Ecoacoustic's AnalysisPrograms.exe. The defaults for each OS are different: + - Windows: will install to HOMEDIR and add the directory to the PATH environment variable. + - Linux will + - install to $HOME/.local/share and symlink to $HOME/.local/bin + - install sox if necessary (requires sudo) + - MacOS: will install to $HOME/.local/share and symlink to $HOME/.local/bin + +AP.exe does not automatically update itself and currently is not integrated with any system package managers. +.PARAMETER Install +Indicates an install should occur. This is implied if none of Install, Check, or Uninstall are supplied. +.PARAMETER Check +Indicates an that the installation of AP should be checked (implied when -Install used) +.PARAMETER Uninstall +Indicates an uninstall should occur +.PARAMETER Prerelease +Get the latest pre-release version instead of the latest stable version +.PARAMETER Version +The AP.exe version to download from GitHub. Must be a specific version (e.g. "18.8.1.2"). If not supplied the latest version is installed. +.PARAMETER Destination +Where to "install" AP.exe to. +.PARAMETER DontAddToPath +By default, the installer will place a shortcut AP.exe in a place where other programs can easily locate it. On Linux it will symlink to from a bin path to the install location. +To prevent this, use the DontAddToPath switch. +.PARAMETER GithubApiToken +An optional token used to avoid rate limiting by the GitHub API. + +If not provided and an environment variable named "GITHUB_AUTH_TOKEN" exists, then the value of GITHUB_AUTH_TOKEN will be used. +.PARAMETER Force +Force overwrite an existing installation +.PARAMETER Quiet +Supress informational messages (overrides $InformationalPreference) +.PARAMETER PassThru +If used will return an object with information about the install result. +.INPUTS +None +.OUTPUTS +An object describing the install if -PassThru specified, otherwise nothing. +.EXAMPLE +./download_ap.ps1 +Install the latest stable version +.EXAMPLE +./download_ap.ps1 -Prerelease +Install the latest prerelease version +.EXAMPLE +./download_ap.ps1 -Version "v20.11.2.0" +Install a specific version +.EXAMPLE +./download_ap.ps1 -Install +Specify the install action explicitly +.EXAMPLE +./download_ap.ps1 -Quiet +Disable status messages +.EXAMPLE +./download_ap.ps1 -Check +Check AP's installation +.EXAMPLE +./download_ap.ps1 -Uninstall +Uninstall AP +.LINK +https://ap.qut.ecoacoustics.info/basics/installing.html +#> + [CmdletBinding( - DefaultParameterSetName = "Default", - HelpURI = "https://github.com/QutEcoacoustics/audio-analysis/blob/master/docs/installing.md", + DefaultParameterSetName = "Install-Latest", + HelpURI = "https://ap.qut.ecoacoustics.info/basics/installing.html", SupportsShouldProcess = $true )] param( - [Parameter(ParameterSetName = "Default")] - [ValidateSet('Stable', 'Weekly', 'Continuous')] - [string]$package = "Stable", + [Parameter(ParameterSetName = "Install-Latest")] + [Parameter(ParameterSetName = "Install-Version")] + [switch]$Install, + [Parameter(ParameterSetName = "Check")] + [switch]$Check, + [Parameter(ParameterSetName = "Uninstall")] + [switch]$Uninstall, - [Parameter(ParameterSetName = "GitHub", Mandatory = $true)] - [string]$version, + [Parameter(ParameterSetName = "Install-Version")] + [string]$Version = $null, - [Parameter(ParameterSetName = "AppVeyor", Mandatory = $true)] - [string]$ci_build_number, + [Parameter(ParameterSetName = "Install-Latest")] + [switch]$Prerelease = $false, + + [Parameter(ParameterSetName = "Install-Latest")] + [Parameter(ParameterSetName = "Install-Version")] + [switch]$FxDependent = $false, + + [Parameter(ParameterSetName = "Install-Latest")] + [Parameter(ParameterSetName = "Install-Version")] + [switch]$DontAddToPath, + + [Parameter(ParameterSetName = "Check")] + [switch]$DontCheckPath, [Parameter()] - [string]$destination = "/AP", + [string]$Destination, # GitHub API token used to bypass rate limiting [Parameter()] [string] - $github_api_token + $GithubApiToken, + + [Switch]$Force, + + [switch]$Quiet, + + [switch]$PassThru ) $ErrorActionPreference = "Stop" +# https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-shouldprocess +if ($Force){ + $ConfirmPreference = 'None' +} +if ($Quiet) { + $InformationPreference = 'SilentlyContinue' +} else { + $InformationPreference = 'Continue' +} +# When pwsh is run without a profile the output encoding reverts to the system code page on Windows +$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +# alias $PsCmdlet. When we have sub functions $PsCmdlet is redefined in them and we need access to the parent value +# we were using the `script:` scope modifier but that doesn't work when this script is evaluated as a dynamic function +# - like when its downloaded and run from the internet +$cmdlet = $PsCmdlet -$build = "Release" -$type = $PsCmdlet.ParameterSetName -$exact_version = $null -switch ($type) { - "Default" { - switch ($package) { - "Stable" { - $source = "github" - } - "Weekly" { - $source = "github" - } - "Continuous" { - $source = "appveyor" - - } - Default { throw "Unkown package type"} - } +function PickForOS($winValue, $linuxValue, $macValue) { + if($IsWindows) { + return $winValue + } elseif ($IsLinux) { + return $linuxValue + } elseif ($IsMacOS) { + return $macValue + } else { + throw "Unknown operating system" } - "GitHub" { - if ($version -notmatch '\d{2}\.\d{1,2}\.\d{1,2}\.\d{1,2}') { - Write-Error "The version argument '$version' did not look like a version (w.x.y.z)" - exit 1 - } - $source = "github" - $exact_version = $version +} + +if (!$Destination) { + $Destination = PickForOS "${home}\AP" "${home}/.local/share/AP" "${home}/.local/share/AP" +} + +$headers = if ($GithubApiToken) { + Write-Debug "Using github auth token from script argument" + @{"Authorization" = "token $GithubApiToken"} + } elseif ($env:GITHUB_AUTH_TOKEN) { + Write-Debug "Using github auth token from environment variable GITHUB_AUTH_TOKEN" + @{"Authorization" = "token $env:GITHUB_AUTH_TOKEN"} + } else { + Write-Debug "Not using a github auth token" + @{} } - "AppVeyor" { - if ($ci_build_number -notmatch '\d{1,4}') { - Write-Error "The ci_build_number argument '$ci_build_number' is not a valid number" - exit 1 - } - $source = "appveyor" - $exact_version = $ci_build_number + +$base_indent = "- " +$indent = " - " +$indent2 = " - " + + +$path_path = PickForOS $Destination "${home}/.local/bin" "${home}/.local/bin" +$path_delimiter = [System.IO.Path]::PathSeparator +$dir_seperator = [System.IO.Path]::DirectorySeparatorChar +$symlink_path = PickForOS $null "${home}/.local/bin/AnalysisPrograms" "${home}/.local/bin/AnalysisPrograms" +$alias_path = PickForOS "${Destination}${dir_seperator}AP.exe" "${home}/.local/bin/AP" "${home}/.local/bin/AP" +$binary_name = PickForOS "AnalysisPrograms.exe" "AnalysisPrograms" "AnalysisPrograms" +$ap_path = "$Destination$dir_seperator$binary_name" +$version_regex = '\d{2}\.\d{1,2}\.\d{1,2}\.\d{1,2}' + +if ($Version) { + + # strip the leading v if it is present + $Version = $Version -replace "^v", "" + + if ($Version -notmatch $version_regex ) { + Write-Error "The version argument '$Version' did not look like a version (w.x.y.z)" + exit 1 } } # resolve metadata for asset -# Begin non-volitile block (this section always runs despite -whatif but it also -# should have no side effects) -$script:oldWhatIfPreference = $WhatIfPreference -try { - $WhatIfPreference = $false - if ($source -eq "github") { - $headers = if ($github_api_token) { @{"Authorization" = "token $github_api_token"} } else { @{} } +function Get-AssetUrl($prerelease ) { + # Begin non-volitile block (this section always runs despite -whatif but it also + # should have no side effects) + $oldWhatIfPreference = $WhatIfPreference + try { + $WhatIfPreference = $false $github_url = "https://api.github.com/repos/QutEcoacoustics/audio-analysis/releases" - if ($null -eq $exact_version) { - if ($package -eq "Stable") { + if (!$Version) { + if (!$prerelease) { $github_url += "/latest" } - else { - # Weekly release i.e. pre-release - # Nothing to do here, we return a list of releases and filter for the - # one we want - } } else { - # strip the leading v if it is present - $exact_version = $exact_version -replace "^v", "" - $github_url += "/tags/v$exact_version" + $github_url += "/tags/v$Version" } + # not supported + #AP_win-x64_Debug_v20.11.2.0.zip + #AP_linux-x64_Debug_v20.11.2.0.tar.xz + #AP_osx-x64_Debug_v20.11.2.0.tar.xz + + $x64 = [Environment]::Is64BitOperatingSystem + $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture + $is_musl = $IsLinux -and ((ldd --version *>&1 | join-string) -match "musl") + + $build = switch ($arch) { + #AP_any_v20.11.2.0.tar.xz + { $FxDependent } { "any_v" } + #AP_linux-musl-x64_v20.11.2.0.tar.xz + {$x64 -and $IsLinux -and $is_musl } { "linux-musl-x64_v" } + #AP_win-arm64_v20.11.2.0.zip + { $_ -eq "Arm64" -and $IsWindows } { "win-arm64_v" } + #AP_linux-arm64_v20.11.2.0.tar.xz + { $_ -eq "Arm64" -and $IsLinux } { "linux-arm64_v" } + #AP_linux-arm_v20.11.2.0.tar.xz + { $_ -eq "Arm" -and $IsLinux } { "linux-arm_v" } + #AP_linux-x64_v20.11.2.0.tar.xz + { $_ -eq "X64" -and $IsLinux } { "linux-x64_v" } + #AP_win-x64_v20.11.2.0.zip + { $_ -eq "X64" -and $IsWindows } { "win-x64_v" } + #AP_osx-x64_v20.11.2.0.tar.xz + { $_ -eq "X64" -and $IsMacOS } { "osx-x64_v" } + default { $null } + } + + Write-Debug "${indent}Build $build chosen, x64: $x64, arch: $arch, is musl: $is_musl" + if ($null -eq $build) { + throw "Unknown platform. We could not detect your platform or we do not have a build for your platform." + } + Write-Debug "${indent}Querying GitHub with $github_url" $response = Invoke-RestMethod -Method Get -Uri $github_url -Headers $headers $response = $response | Select-Object -First 1 $asset_url = $response.assets ` - | Where-Object { $_.name -like "$build*" } ` + | Where-Object { $_.name -like "*$build*" } ` | ForEach-Object browser_download_url - $exact_version = $response.tag_name - Write-Output "Downloading release $($response.tag_name) from GitHub" - } - elseif ($source -eq "appveyor") { - $appveyor_api = "https://ci.appveyor.com/api" - $appveyor_project_url = "$appveyor_api/projects/QUTEcoacousticsResearchGroup/audio-analysis" - - # get the last 50 builds - $response = Invoke-RestMethod -Method Get -Uri "$appveyor_project_url/history?recordsNumber=50" - - # filter builds for master branch and build success - $ci_builds = $response.builds ` - | Where-Object { $_.status -eq "success" -and $_.branch -eq "master" } ` - | Sort-Object finished -Descending - if ($null -eq $exact_version) { - # "Continuous" package - $ci_build = $ci_builds[0] + + if($asset_url.Count -gt 1) { + throw "More than one asset matched criteria, need one url to continue. Urls: $asset_url" + } + + $final_version = $response.tag_name -replace "^v","" + $is_prerelease = $response.prerelease + Write-Information "${indent}Found release $($response.tag_name) from GitHub" + return [pscustomobject]@{Version = $final_version ; Url = $asset_url ; IsPrerelease = $is_prerelease} + } + finally { + $WhatIfPreference = $oldWhatIfPreference + } +} + +function New-InstallDirectory { + # remove directory if it already exists + if (Test-Path $Destination) { + $message = "${indent}There was an existing install of AP. Continuing`n${indent2}will delete config files`n${indent2}will NOT delete log files`n${indent2}will delete everything else" + if($Force -or $cmdlet.ShouldContinue("Deleting existing folder ``$Destination``?", $message)) { + # hardcoding because this script can be executed anonymously - in that case it doesn't have a name we can use + $script_name = "download_ap.ps1" + Get-ChildItem -Path "$Destination" -Exclude "$script_name","Logs" | Remove-Item -Recurse -ErrorAction Stop + Start-Sleep 1 } else { - $ci_build = $ci_builds | Where-Object { $_.version -eq $exact_version } + Write-Error "User cancelled installation" + return $false } + } + else { + New-Item $Destination -ItemType Directory -ErrorAction Stop -Force | Write-Debug + return $true + } +} - if ($null -eq $ci_build) { - throw "could not find version '$exact_version' in last 50 AppVeyor builds" +function Get-Asset($asset_url) { + if(!$asset_url) { throw "`$asset_url null or empty" } + # download asset + Write-Information "${indent}Downloading $final_version" + $extension = switch ($asset_url) { + { $_.EndsWith(".zip") } { ".zip" } + { $_.EndsWith(".tar.xz") } { ".tar.xz" } + Default { "Unspported extension for $asset_url"} + } + $downloaded_zip = "$Destination${dir_seperator}AP$extension" + Write-Debug "${indent}Downloading $asset_url to $downloaded_zip" + if ($cmdlet.ShouldProcess($asset_url, "Downloading asset")) { + $curl = Get-Command curl, curl.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($curl) { + $curl_headers = @($headers.GetEnumerator() | ForEach-Object { "-H", "'$($_.Key): $($_.Value)'" }) + & $curl @curl_headers -L -o "$downloaded_zip" "$asset_url" | ForEach-Object { "${index}$_" } | Write-Information + if ($LASTEXITCODE -ne 0) { + throw "Failed dowloading $asset_url using $curl" + } + } + else { + $ProgressPreference = 'SilentlyContinue' + Invoke-RestMethod $asset_url -OutFile $downloaded_zip -TimeoutSec 60 -MaximumRetryCount 3 -Headers $headers } + } - # now get the build (we need to do this again because the job sub-object) - # is not included in the build objects when the history is retrieved - $ci_build = (Invoke-RestMethod -Method Get -Uri "$appveyor_project_url/build/$($ci_build.version)").build - $artifacts_url = "$appveyor_api/buildJobs/$($ci_build.jobs[0].jobId)/artifacts" - $artifacts = Invoke-RestMethod -Method Get -Uri $artifacts_url + # extract asset + if ($cmdlet.ShouldProcess($downloaded_zip, "Extracting AP.exe")) { + if ($extension -eq ".tar.xz") { + # we should be only mac/linux at this branch + tar --extract --xz --file $downloaded_zip --directory $Destination + if ($LASTEXITCODE -ne 0) { + throw "Failed extracting $downloaded_zip using tar" + } + } + elseif (Get-Command unzip -CommandType Application -ErrorAction SilentlyContinue) { + unzip -q -o $downloaded_zip -d $Destination - $file_name = ($artifacts | Where-Object { $_.fileName -like "*$build*" }).fileName + if ($LASTEXITCODE -ne 0) { + throw "Failed extracting $downloaded_zip using unzip" + } + } + else { + Import-Module "Microsoft.PowerShell.Archive" -Force + Microsoft.PowerShell.Archive\Expand-Archive -LiteralPath $downloaded_zip -DestinationPath $Destination -Force - $asset_url = "$artifacts_url/$file_name" - $exact_version = $ci_build.version - Write-Output "Downloading version $($ci_build.version) from AppVeyor" - } - else { - throw "unknown source '$source'" + } + Remove-Item $downloaded_zip } -} -finally { - $WhatIfPreference = $script:oldWhatIfPreference + + Write-Information "${indent}Download complete, installed to $Destination" } -# -# Begin volitile actions -# +function Remove-InstallDirectory { + if (Test-Path $Destination) { + if($Force -or $cmdlet.ShouldContinue("Are you sure you want to delete the folder ``$Destination``?", "${indent}Continuing will delete everything in $Destination")) { -# remove directory if it already exists -$destination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($destination) -if (Test-Path $destination) { - Write-Warning "Deleting old installation at '$destination' (but keeping log files)" - if ($PsCmdlet.ShouldProcess($destination, "Deleting old install")) { - $script_name = split-path $PSCommandPath -Leaf - Remove-Item -Path "$destination/*" -Recurse -ErrorAction Stop -Exclude "$script_name", "*log*" - Start-Sleep 1 + Remove-Item $Destination -Recurse -Force + Start-Sleep 1 + return $true + } + else { + Write-Error "User cancelled uninstall" + return $false + } + } + else { + Write-Information "${indent}$Destination does not exist. Nothing to do." + return $null } -} -else { - New-Item $destination -ItemType Directory -ErrorAction Stop } +function Test-Sox { + !!(Get-Command "sox" -CommandType Application -ErrorAction SilentlyContinue) +} +function Install-SoxOnLinux { + if (!$IsLinux) { + throw "Install SoX only works on Linux" + } + if($Force -or $cmdlet.ShouldContinue("Install SoX?", "${indent}SoX not detected. We will attempt to install it")) { + $package_manager = "apt-get", "yum" | Get-Command -CommandType Application -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name -First 1 + $package_manager += switch ($package_manager) { + "apt-get" { " -o=Dpkg::Use-Pty=0 -qq -y" } # suppress progress bar + "yum" { " -q -y" } + Default { + throw "Could not detect package manager: $package_managage" + } + } -# download asset -Write-Output "Downloading asset $asset_url" -$downloaded_zip = "$destination/AP.zip" -if ($PsCmdlet.ShouldProcess($asset_url, "Downloading asset")) { - # use curl if available (faster) - $curl = Get-Command curl, curl.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($curl) { - & $curl -L -o "$downloaded_zip" "$asset_url" + # wild west here + Invoke-Expression "sudo $package_manager install sox" *>&1 | Write-Information -OutBuffer 1000 if ($LASTEXITCODE -ne 0) { - throw "Failed dowloading $asset_url using $curl" + throw "Failed installing sox with $package_manager" } + + return $true } else { - $asset_response = Invoke-WebRequest $asset_url -OutFile $downloaded_zip -PassThru - if ($asset_response.StatusCode -ne 200) { - throw "Failed downloading $asset_url" - } + Write-Error "User cancelled installation" } } -# extract asset -if ($PsCmdlet.ShouldProcess($downloaded_zip, "Extracting AP.exe zip")) { - if (Get-Command unzip -CommandType Application -ErrorAction SilentlyContinue) { - unzip -q -o $downloaded_zip -d $destination +function Test-ApDirInPath { + # test path in PATH, make sure path matches wholly and not a subset of a path + # by splitting the path on it's delimiter + ($env:PATH -split $path_delimiter) -contains $path_path +} - if ($LASTEXITCODE -ne 0) { - throw "Failed extracting $downloaded_zip using unzip" - } + +# ensure updates to these variables in nested scopes propagate back to this scope +"installed_version", "install_info", "update_info" | Foreach-Object { Set-Variable -Name $_ -Value $null -Option AllScope } + +$actions = [ordered]@{ + "Check User Directory" = @{ + "Install" = $null + "Check" = ("User home directory is found at $home", { Test-Path $home } ) + "Uninstall" = $null } - else { - Import-Module "Microsoft.PowerShell.Archive" -Force - Microsoft.PowerShell.Archive\Expand-Archive -LiteralPath $downloaded_zip -DestinationPath $destination -Force - + "Resolve asset" = @{ + "Install" = ("Querying GitHub for releases", { + $install_info = Get-AssetUrl $Prerelease + return $install_info.Url.Length -gt 0 + + } ) + "Check" = ("Querying GitHub for updates", { + $update_info = Get-AssetUrl $true + return $true + } ) + "Uninstall" = $null + } + "Install Directory" = @{ + "Install" = ({"Installing to $Destination"}, { New-InstallDirectory } ) + "Check" = ({"AP directory $Destination exists"}, { Test-Path $Destination } ) + "Uninstall" = ({"Deleting all files in $Destination"}, { Remove-InstallDirectory } ) + } + "Place AP" = @{ + "Install" = ({"Downloading to $Destination"}, { + Get-Asset $install_info.Url + return $true + } ) + "Check" = ({"AP executable should be in $Destination"}, { Test-Path -PathType Leaf $ap_path } ) + "Uninstall" = $null + } + "Check permissions" = @{ + "Install" = ("Allow AP to have execute permission", { + if($IsWindows) { return $null } chmod +x $ap_path + if ($LASTEXITCODE -ne 0) { return $false } else { return $true } + } ) + "Check" = ("AP executable should have execute permission", { $(test -x $ap_path ; 0 -eq $LASTEXITCODE) } ) + "Uninstall" = $null + } + "Check AP runs" = @{ + "Install" = $null + "Check" = ( + "AP should be able to run", + { + $installed_version = & "$ap_path" --version | Select-Object -Last 1 + return $LASTEXITCODE -eq 0 + } + ) + "Uninstall" = $null + } + "Check version" = @{ + "Install" = $null + "Check" = ( + { "AP version $installed_version should match desired version $Version" }, + { + if ($Version) { + return $installed_version -eq $Version + } else { + return $installed_version -match $version_regex + } + } + ) + "Uninstall" = $null + } + "Is SoX available" = @{ + "Install" = ("Install SoX", { + if (!$IsLinux) { return $null } + if (Test-Sox) { return $true } + return Install-SoxOnLinux + } ) + "Check" = ({ "Check SoX is available" }, { if (!$IsLinux) { return $null } else { return Test-Sox } }) + "Uninstall" = $null + } + "AP checks its environment" = @{ + "Install" = $null + "Check" = ("AP checking its environment", { $output = & "$ap_path" CheckEnvironment ; if ($LASTEXITCODE -ne 0) { $output | Write-Error ; return $false } else { return $true } } ) + "Uninstall" = $null + } + "Modify Path" = @{ + "Install" = ({"Add $path_path to `$PATH"}, { + if ($DontAddToPath) { return $null } + if (Test-ApDirInPath) { return $true } + $to_add = $path_delimiter + $path_path + if ($IsWindows) { + $new_path = [System.Environment]::GetEnvironmentVariable("Path","User") + $to_add + [System.Environment]::SetEnvironmentVariable("Path", $new_path, "User") + } else { + # bash syntax + "PATH=`$PATH$to_add`n" | Out-File "$HOME/.profile" -Encoding utf8NoBOM -Append + } + # update current process path as well + $env:PATH += $to_add + return $true + } ) + "Check" = ({"The `$PATH variable contains the path $path_path"}, { + if ($DontCheckPath) { return $null } + Test-ApDirInPath + } ) + "Uninstall" = ("Removing $path_path from `$PATH variable", { + if(!$IsWindows) { + # the path added to PATH for mac/linux is a general system bin path + # - we shouldn't mess with it or remove it, even if we added it + return $null + } + if (Test-ApDirInPath) { + $to_remove = "(^|$path_delimiter)" + [Regex]::Escape($path_path) + if ($IsWindows) { + $new_path = [System.Environment]::GetEnvironmentVariable("Path","User") -replace $to_remove,"" + [System.Environment]::SetEnvironmentVariable("Path", $new_path, "User") + } else { + # bash syntax + $target = "PATH=`$PATH:$path_path`n" + (Get-Content "$HOME/.profile" -Raw) -replace $target,"`n" | Out-File "$HOME/.profile" -Encoding utf8NoBOM -Append + } + # update current process path as well + $env:PATH = $env:PATH -replace $to_remove,"" + return $true + } else { + return $null + } + } ) + } + "Symlink for AP" = @{ + "Install" = ({"Symlink $symlink_path to $ap_path"}, { if($null -eq $symlink_path) { return $null } New-Item -Type SymbolicLink -Force -Path $symlink_path -Target $ap_path | Out-Null ; return $true } ) + "Check" = ({"$symlink_path exists and points to $ap_path"}, { + if($null -eq $symlink_path) { return $null } + return (Test-Path $symlink_path) ` + -and ((Get-Item $symlink_path).LinkType -eq 'SymbolicLink') ` + -and ((Get-Item $symlink_path).Target -eq $ap_path) + } ) + "Uninstall" = ("Removing symlink", { + if($null -eq $symlink_path) { return $null } + if(Test-Path $symlink_path) { + Remove-Item $symlink_path + return $true + } + else { + Write-Debug "symlink path $symlink_path does not exist, skipping" + return $true + } + } ) + } + "Symlink alias for AP" = @{ + "Install" = ({"Symlink $alias_path to $ap_path"}, { if($null -eq $alias_path) { return $null } New-Item -Type SymbolicLink -Force -Path $alias_path -Target $ap_path | Out-Null ; return $true } ) + "Check" = ("$alias_path exists and points to $ap_path", { + return (Test-Path $alias_path) ` + -and ((Get-Item $alias_path).LinkType -eq 'SymbolicLink') ` + -and ((Get-Item $alias_path).Target -eq $ap_path) + } ) + "Uninstall" = ("Removing alias symlink", { + if(Test-Path $alias_path) { + Remove-Item $alias_path + return $true + } + else { + Write-Debug "alias path $alias_path does not exist, skipping" + return $null + } + } ) } - Remove-Item $downloaded_zip } -Write-Output "Download complete, installed to $destination" - - -$IsWin = $IsWindows -or ($PSVersionTable.PSVersion.Major -lt 6) -if ($IsWin) { - $paths = [Environment]::GetEnvironmentVariables("User").Path -split [IO.Path]::PathSeparator - if ($paths -notcontains $destination) { - Write-Output "Adding AP.exe to PATH" - $paths = $paths | Where-Object { $_ } - $paths += $destination - $user_path = ($paths -join [IO.Path]::PathSeparator) - if ($PsCmdlet.ShouldProcess("PATH", "Adding $destination to PATH")) { - [Environment]::SetEnvironmentVariable("Path", $user_path, "User") - # this change to so that the process env vars have access - $env:Path += ([IO.Path]::PathSeparator + $destination) +function Invoke-APInstallTask($current, $key) { + if ($null -eq $current) { + Write-Debug "${indent} no task for $key, skipped" + return + } + + $message, $task = $current + $err = $null + $result = $null + try { + # using & operator to evaluate over Invoke-Command, because it avoids a + # layer of indirection and makes tracking down errors easier + $result = & $task + } + catch { + $err = $_ + } + + if ($message -is [scriptblock]) { + $message = & $message + } + + switch ($result) { + { $err } { + Write-Error "${indent}❌ [error] AP's installation has a problem: $message`n Error:$($err | Out-String)" + exit 1 + } + $false { + Write-Error "${indent}❌ [error] AP's installation has a problem: $message failed" + exit 1 + } + $null { Write-Information "${indent}➖ [skipped] $message" } + $true { Write-Information "${indent}✅ [success] $message" } + default { + throw "unexpected result: $result" } } } -# TODO Unix/MacOs symlinking -Write-Output "Checking environment has installed dependencies" -$check_environment = $null -$ErrorActionPreference = "Continue" -if ($PsCmdlet.ShouldProcess("AnalysisPrograms.exe", "Checking the install")) { - $command = "$destination/AnalysisPrograms.exe CheckEnvironment" - if (! $IsWin) { - - $command = "mono " + $command - } - if (! $IsWin -and $null -eq (Get-Command mono -CommandType Application -ErrorAction SilentlyContinue) ) { - Write-Error "Mono was not found on PATH. It must be isntalled for AP.exe to work" - $check_environment = -1 +function Invoke-APInstallTasks($actions, $key) { + if (!$key) { + throw "`$key was null or empty: $key" } - else { - Invoke-Expression $command - $check_environment = $LASTEXITCODE + + foreach($step in $actions.GetEnumerator()) { + $step_name, $tasks = $step.Key, $step.Value + $current = $tasks[$key] + + # if installing then automatically run the check task after each install task + $next = if ("Install" -eq $key) { $tasks["Check"] } else { $null } + + if ($current -or $next) { + Write-Information ($base_indent + $step_name) + } + + Invoke-APInstallTask $current $key + + if ($next) { + Invoke-APInstallTask $next "Check" + } } +} + +function Write-Metadata() { + if ($PassThru) { - if ($check_environment -ne 0) { - Write-Error "Unable to run AP.exe. There is some problem with your setup." - Write-Warning "You can run the environment check again by executing:`n`t$command" + $resolved_version = $final_version + $resolved_version = $resolved_version -replace "^v", "" + + return [PSCustomObject]@{ + InstallMetadata = $install_info + UpdateInfo = $update_info + UpdateAvailable = if($update_info -and $installed_version) { [version]"$($update_info.Version)" -gt [version]"$installed_version" } else { $null } + InstalledVersion = $installed_version + InstalledDestination = $Destination + } } } -return [PSCustomObject]@{ - Type = $type - Source = $source - ResolvedVersion = $exact_version - Destination = $destination - AssetUrl = $asset_url - EnvironmentCheck = $check_environment +switch -wildcard ($PsCmdlet.ParameterSetName) { + "Install-*" { + Invoke-APInstallTasks $actions "Install" + Write-Information "✅ Installed AP $installed_version" + } + "Check" { + Invoke-APInstallTasks $actions "Check" + Write-Information "✅ Check complete for AP version $installed_version" + } + "Uninstall" { + Invoke-APInstallTasks $actions "Uninstall" + Write-Information "✅ Uninstalled AP" + } + default { + Write-Output "No action selected, no action taken, exiting" + exit 1 + } } + +return Write-Metadata diff --git a/docs/basics/installing.md b/docs/basics/installing.md index d65811e16..9a18df1b9 100644 --- a/docs/basics/installing.md +++ b/docs/basics/installing.md @@ -3,11 +3,8 @@ uid: basics-installing --- # Installing -## Beginner Tutorial - If you're new to using _AP.exe_ we recommend following the instructions -in the practical -to setup and install _AP.exe_ +in the practical. ## Supported Platforms @@ -25,41 +22,99 @@ to setup and install _AP.exe_ The automatic install will download AP.exe and may install required perquisites. -1. Prerequisite: install Powershell 6+ - - Go [here](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7) to install PowerShell -2. Run the following command in a PowerShell prompt: +1. Prerequisite: install Powershell 7+ + - Go to + to find instructions for installing PowerShell +2. Then: + + + +### [Windows install](#tab/windows) -# [Windows](#tab/windows-automatic) Run the following command in an elevated (_Run as Administrator_) prompt: +```powershell +'$function:i=irm "https://git.io/JtOo3";i' | pwsh -nop -ex B -c - ``` -PowerShell -NoProfile -ExecutionPolicy Bypass -Command "$t = \"$env:Temp\download_ap.ps1\"; (New-Object System.Net.WebClient).DownloadFile('https://raw.githubusercontent.com/QutEcoacoustics/audio-analysis/master/build/download_ap.ps1', $t); & $t; rm $t" + +Or, to install the prerelease version: + +```powershell +'$function:i=irm "https://git.io/JtOo3";i -Pre' | pwsh -nop -ex B -c - ``` -# [Linux](#tab/linux-automatic) + +### [Linux install](#tab/linux) + Run the following command: +```bash +echo '$function:i=irm "https://git.io/JtOo3";i' | pwsh -nop -c - ``` -sudo pwsh -NoProfile -ExecutionPolicy Bypass -Command "\$t = \"\$env:Temp/download_ap.ps1\"; (New-Object System.Net.WebClient).DownloadFile('https://raw.githubusercontent.com/QutEcoacoustics/audio-analysis/master/build/download_ap.ps1', \$t); & \$t; rm \$t" + +Or, to install the prerelease version: + +```bash +echo '$function:i=irm "https://git.io/JtOo3";i -Pre' | pwsh -nop -c - ``` -# [MacOSX](#tab/osx-automatic) + +### [MacOSX install](#tab/osx) + Run the following command in _Terminal_: +```bash +echo '$function:i=irm "https://git.io/JtOo3";i' | sudo pwsh -nop -c - ``` -sudo pwsh -NoProfile -ExecutionPolicy Bypass -Command "\$t = \"\$env:Temp/download_ap.ps1\"; (New-Object System.Net.WebClient).DownloadFile('https://raw.githubusercontent.com/QutEcoacoustics/audio-analysis/master/build/download_ap.ps1', \$t); & \$t; rm \$t" + +Or, to install the prerelease version: + +```bash +echo '$function:i=irm "https://git.io/JtOo3";i -Pre' | sudo pwsh -nop -c - ``` -*** -
+*** > [!NOTE] -> Please inspect -> https://github.com/QutEcoacoustics/audio-analysis/blob/master/build/download_ap.ps1 -> prior to running to ensure safety. We already know it's safe, but you should verify +> Please inspect which should point to +> +> prior to running these commands. +> +> We already know the script is safe, but you should verify > the security and contents of any script from the internet you are not familiar > with. The above command downloads a remote PowerShell script and executes it on > your machine. +> [!WARN] +> The installer script is brand new. There may be bugs. No warranties provided. + +## Uninstall + +If you used our automatic install you can use the same script to uninstall: + +### [Windows uninstall](#tab/windows) + +Run the following command in an elevated (_Run as Administrator_) prompt: + +```powershell +'$function:i=irm "https://git.io/JtOo3";i -Un' | pwsh -nop -ex B -c - +``` + +### [Linux uninstall](#tab/linux) + +Run the following command: + +```bash +echo '$function:i=irm "https://git.io/JtOo3";i -Un' | pwsh -nop -c - +``` + +### [MacOSX uninstall](#tab/osx) + +Run the following command in _Terminal_: + +```bash +echo '$function:i=irm "https://git.io/JtOo3";i -Un' | sudo pwsh -nop -c - +``` + ## Manual Install 1. Go to our [releases](https://github.com/QutEcoacoustics/audio-analysis/releases) page @@ -70,32 +125,37 @@ sudo pwsh -NoProfile -ExecutionPolicy Bypass -Command "\$t = \"\$env:Temp/downlo 4. Download the version of AnalysisPrograms suitable for your computer (see [Choosing the asset](#choosing-the-asset)) 5. Extract the folder - It can be installed in any directory - - We typically extract to a directory named `C:\AP` or `/AP` on Linux -7. Make sure any [Prerequisites](#prerequisites) are installed -6. Finally, check the install by running: + - We typically extract to a directory named `~\AP` or `~/.local/share/AP` on Linux +6. Make sure any [Prerequisites](#prerequisites) are installed +7. [Optional] Add the install directory to your PATH environment variable +8. Finally, check the install by running: Run the following command: -# [Windows](#tab/windows-automatic) -```bash +### [Windows Check](#tab/windows) + +```powershell C:\AP\AnalysisPrograms.exe CheckEnvironment ``` -# [Linux](#tab/linux-automatic) + +### [Linux Check](#tab/linux) + ```bash /AP/AnalysisPrograms CheckEnvironment ``` -# [MacOSX](#tab/osx-automatic) + +### [MacOSX Check](#tab/osx) + ```bash /AP/AnalysisPrograms CheckEnvironment ``` -*** +*** ### Choosing the asset [!include[](<./assetChooser.html>)] - ## Prerequisites ### Windows @@ -104,40 +164,28 @@ None. Self contained download. ### MacOSX - None. Self contained download. ### Linux/Unix The following additional dependencies may be required for Linux/Unix machines: -- **MAYBE**: ffmpeg +- **MAYBE**: ffmpeg - a packaged version with AP.exe should work for all platforms except ARM and ARM64 - **MAYBE**: wavpack - libsox-fmt-all, sox - libav-tools (on some distros only, not needed in Ubuntu 18) - ## Build Packages -There are three packages AP.exe: +There are two variants of AP.exe: 1. The **Stable** release is well tested used by QUT Ecoacoustics on our servers and is usually a few months old -2. The **Weekly** release is automatically built every Monday. It has more +2. The **Prerelease** release is automatically built weekly, every Monday. It has more features and bug fixes than the stable release but it also could have more bugs. -3. The **Continuous** package is created every time there is a change to our - code. It is the bleeding edge: always up to date but conversely the most - likely to have bugs. You should use the **Stable** release unless there is a specific feature or bug you need. - - - - - - -