Public/New-FirebirdEnvironment.ps1

function New-FirebirdEnvironment {
    <#
    .SYNOPSIS
        Downloads and extracts Firebird binaries for a given version and platform.
    .DESCRIPTION
        Installs Firebird to a specified or temporary directory and returns environment details.
        Supports both official releases (by version) and snapshot builds (by branch).
    .PARAMETER Version
        Firebird version to install. Minimum supported version is 3.0.9.
    .PARAMETER Branch
        Snapshot branch to install from (e.g. 'master' for Firebird 6.x development builds,
        'v5.0-release' for Firebird 5.x next-patch builds, 'v4.0' for Firebird 4.x next-patch builds).
        Mutually exclusive with -Version.
    .PARAMETER Path
        Directory to extract the binaries to. Uses a temporary folder if not provided.
    .PARAMETER RuntimeIdentifier
        Runtime identifier for the platform. Uses current platform if not specified.
    .PARAMETER Force
        Overwrites the output directory if it already exists.
    .EXAMPLE
        New-FirebirdEnvironment -Version 5.0.2 -Path '/tmp/firebird-5.0.2' -Force
        Installs Firebird 5.0.2 to the specified path, overwriting if it exists.
    .EXAMPLE
        New-FirebirdEnvironment -Version 5.0.2
        Installs Firebird 5.0.2 to a temporary directory.
    .EXAMPLE
        New-FirebirdEnvironment -Branch 'master'
        Installs the latest Firebird snapshot from the master branch to a temporary directory.
    .OUTPUTS
        FirebirdEnvironment object with Path and Version properties.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'ByVersion')]
    [OutputType([FirebirdEnvironment])]
    Param(
        [Parameter(Mandatory, ParameterSetName = 'ByVersion')]
        [semver]$Version,

        [Parameter(Mandatory, ParameterSetName = 'ByBranch')]
        [ValidateSet('master', 'v5.0-release', 'v4.0')]
        [string]$Branch,

        [string]$Path,

        [ValidateSet('win-x86', 'win-x64', 'win-arm64', 'linux-x64', 'linux-arm64')]
        [string]$RuntimeIdentifier,

        [switch]$Force
    )

    if (-not $IsWindows -and -not $IsLinux) {
        throw "Unsupported platform: $([System.Runtime.InteropServices.RuntimeInformation]::OSDescription). Only Windows and Linux are supported."
    }

    if ($IsLinux -and (-not (Get-Command 'apt-get' -ErrorAction SilentlyContinue))) {
        throw 'apt-get command not found. Ensure you are running this on a Debian-based Linux distribution.'
    }

    $rid = if ($RuntimeIdentifier) { $RuntimeIdentifier } else { [System.Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier }
    $supportedRIDs = @('win-x86', 'win-x64', 'win-arm64', 'linux-x64', 'linux-arm64')
    if ($supportedRIDs -notcontains $rid) {
        throw "Unsupported RuntimeIdentifier: $rid. Supported: $($supportedRIDs -join ', ')"
    }
    Write-VerboseMark -Message "RuntimeIdentifier is '$rid'."

    # Resolve release info based on parameter set
    if ($PSCmdlet.ParameterSetName -eq 'ByBranch') {
        Write-VerboseMark -Message "Requested Firebird snapshot branch '$Branch'"
        $snapshotInfo = Find-FirebirdSnapshotRelease -Branch $Branch -RuntimeIdentifier $rid
        # Extract version from the snapshot filename (e.g. 'Firebird-6.0.0.1884-...' -> '6.0.0')
        if ($snapshotInfo.FileName -match 'Firebird-(\d+\.\d+\.\d+)') {
            $Version = [semver]$Matches[1]
        } else {
            throw "Cannot determine version from snapshot filename: $($snapshotInfo.FileName)"
        }
        Write-VerboseMark -Message "Resolved snapshot version: $Version"
    } else {
        Write-VerboseMark -Message "Requested Firebird version '$($Version)'"
    }

    $minVersion = [semver]'3.0.9'
    if ($Version -lt $minVersion) {
        throw 'Firebird minimal supported version is 3.0.9.'
    }

    $tempRoot = [System.IO.Path]::GetTempPath()

    if (-not $Path) {
        $pathSuffix = if ($PSCmdlet.ParameterSetName -eq 'ByBranch') { "firebird-snapshot-$Branch" } else { "firebird-$($Version)" }
        $Path = Join-Path $tempRoot $pathSuffix
        Write-VerboseMark -Message "No Path specified. Using temporary folder: $Path"
    }

    if (Test-Path $Path) {
        if (-not $Force) {
            Write-VerboseMark -Message "Path '$Path' already exists and -Force not specified."

            # Check if the existing path is a valid Firebird environment
            $existingEnvironment = Get-FirebirdEnvironment -Path $Path

            # Check if the existing environment version matches the requested version (discard Revision/Build number)
            $v = $existingEnvironment.Version
            $existingVersion = [semver]::new($v.Major, $v.Minor, $v.Build)
            if ($existingVersion -ne $Version) {
                throw "Path '$Path' already exists with version '$($existingVersion)'. Cannot install version '$Version'. Use -Force to overwrite."
            }

            # If the existing environment matches the requested version, return it
            return $existingEnvironment
        }
        if ($PSCmdlet.ShouldProcess($Path, 'Clear existing output directory')) {
            Remove-Item $Path -Recurse -Force
        }
    }

    if ($PSCmdlet.ShouldProcess($Path, 'Create output directory')) {
        New-Item -ItemType Directory $Path -Force > $null
    }

    if ($PSCmdlet.ParameterSetName -eq 'ByBranch') {
        # Snapshot release info was already resolved above
        $releaseInfo = $snapshotInfo
    } else {
        $releaseInfo = Get-FirebirdReleaseUrl -Version $Version -RuntimeIdentifier $rid
    }
    $downloadUrl = $releaseInfo.Url
    Write-VerboseMark -Message "Release URL is '$($downloadUrl)'"

    $archiveFile = $releaseInfo.FileName
    $fullArchiveFile = Join-Path $tempRoot $archiveFile

    if ($PSCmdlet.ShouldProcess($archiveFile, 'Downloading Firebird archive')) {
        Write-VerboseMark -Message "Downloading Firebird archive '$archiveFile'..."
        Invoke-WebRequest $downloadUrl -OutFile $fullArchiveFile -Verbose:$false
    }

    if ($PSCmdlet.ShouldProcess($archiveFile, 'Extracting archive')) {
        Write-VerboseMark -Message "Extracting archive '$archiveFile'..."
        if ($IsWindows) {
            Write-VerboseMark -Message 'Extracting Windows archive...'
            Expand-Archive -Path $fullArchiveFile -DestinationPath $Path
        } elseif ($IsLinux) {
            Write-VerboseMark -Message 'Extracting Linux archive...'
            Invoke-ExternalCommand {
                & tar --extract --file=$fullArchiveFile --gunzip --directory=$Path --strip-components=1
            } -ErrorMessage "Failed to extract '$fullArchiveFile' archive. Cannot continue."

            if (-not ($rid.Contains('linux-arm64') -and ($Version.Major -lt 5))) {
                # For Firebird 5.x+ and non-ARM64, extract the nested buildroot archive.
                Write-VerboseMark -Message 'Extracting buildroot archive...'
                Invoke-ExternalCommand {
                    & tar --extract --file="$Path/buildroot.tar.gz" --gunzip --directory=$Path --strip-components=3 ./opt
                } -ErrorMessage "Failed to extract '$fullArchiveFile' archive. Cannot continue."
            }
        }
    }

    if ($PSCmdlet.ShouldProcess($fullArchiveFile, 'Removing archive')) {
        Write-VerboseMark -Message "Removing archive '$fullArchiveFile'..."
        Remove-Item -Path @(
            # On Linux, also remove the buildroot archive
            "$Path/buildroot.tar.gz",

            # Common
            $fullArchiveFile
        ) -Recurse -Force -ErrorAction Ignore
    }

    # Windows-only: Set the IpcName in firebird.conf
    if ($IsWindows) {
        $ipcName = "FIREBIRD-$($Version -replace '\.','_')"
        $firebirdConfPath = Join-Path $Path 'firebird.conf'
        if ($PSCmdlet.ShouldProcess($firebirdConfPath, "Setting IpcName to '$ipcName' in firebird.conf")) {
            Write-VerboseMark -Message "Setting IpcName to '$ipcName' in firebird.conf..."
            $content = Get-Content $firebirdConfPath
            $content = $content -replace '#IpcName = FIREBIRD', "IpcName = $ipcName"
            Set-Content -Path $firebirdConfPath -Value $content -Encoding Ascii
        }
    } else {
        Write-VerboseMark -Message 'Skipping IpcName configuration (not Windows).'
    }

    # Linux-only: Download additional packages and extract it to the `lib` directory.
    if ($IsLinux) {
        Write-VerboseMark -Message 'Downloading additional Linux packages...'
        $libPath = Join-Path $Path 'lib'

        Invoke-AptDownloadAndExtract -PackageName 'libtommath1' `
            -SourcePattern './usr/lib/*/*' `
            -TargetFolder $libPath

        # For FB3, we also need to download libncurses5
        if ($Version -ge [semver]3) {
            Write-VerboseMark -Message 'Downloading libncurses5 and libtinfo5 for Firebird 3+...'
            Invoke-AptDownloadAndExtract -PackageName 'libncurses5' `
                -SourcePattern './lib/*/*' `
                -TargetFolder $libPath

            Invoke-AptDownloadAndExtract -PackageName 'libtinfo5' `
                -SourcePattern './lib/*/*' `
                -TargetFolder $libPath
        }

        # Fix libtommath for FB3 and FB4 -- https://github.com/FirebirdSQL/firebird/issues/5716#issuecomment-826239174
        if ($Version -lt [semver]5) {
            Write-VerboseMark -Message 'Applying libtommath symlink fix for Firebird < 5...'
            if ($PSCmdlet.ShouldProcess("$libPath/libtommath.so.1", 'Creating symlink for libtommath.so.0...')) {
                Write-VerboseMark -Message 'Creating symlink for libtommath.so.0...'
                ln -sf "$libPath/libtommath.so.1" "$libPath/libtommath.so.0"
            }
        }
    }

    # Remove the sample database from databases.conf
    $databasesConfPath = Join-Path $Path 'databases.conf'
    if (-not (Test-Path $databasesConfPath)) {
        $folderItems = Get-ChildItem -Path $Path
        throw "databases.conf not found at '$Path'. Folder content is: $folderItems"
    }
    
    if ($PSCmdlet.ShouldProcess($databasesConfPath, 'Removing sample database')) {
        Write-VerboseMark -Message "Removing sample database from '$databasesConfPath'..."
        $content = Get-Content $databasesConfPath
        $content | Where-Object { $_ -notmatch '^employee' } | Set-Content $databasesConfPath
    }

    # Clean up the output directory
    if ($PSCmdlet.ShouldProcess($Path, 'Cleaning up output directory')) {
        Write-VerboseMark -Message 'Cleaning up output directory...'
        Remove-Item -Path @(
            # Windows-specific
            "$Path/system32",
            "$Path/*.bat",

            # Linux-specific
            "$Path/buildroot.tar.gz",

            # Common files
            "$Path/doc",
            "$Path/examples",
            "$Path/help",
            "$Path/include",
            "$Path/misc",

            $fullArchiveFile
        ) -Recurse -Force -ErrorAction Ignore
    }

    # Return the environment information as a FirebirdEnvironment class instance.
    Get-FirebirdEnvironment -Path $Path
}