UnityPackageArchiver.psm1

<#
.Synopsis
    Unpack .unitypackage file.
.PARAMETER UnityPackagePath
    path to .unitypackage file
.PARAMETER OutputDir
    output directory path(default: same directory as .unitypackage file)
.EXAMPLE
    Expand-UnityPackage -UnityPackagePath "C:\path\to\package.unitypackage" -OutputDir "C:\output\directory"
#>

function Expand-UnityPackage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$UnityPackagePath,

        [Parameter(Mandatory = $false)]
        [string]$OutputDir = ""
    )

    # Validate
    if (-not (Get-Command tar -ErrorAction SilentlyContinue)) {
        throw "The 'tar' command is not available. Please ensure that 'tar' is installed and accessible in your PATH."
    }

    if (-not (Test-Path -Path $UnityPackagePath)) {
        throw "The input file does not exist: $UnityPackagePath"
    }

    $unityPackageExtension = [System.IO.Path]::GetExtension($UnityPackagePath)
    if ($unityPackageExtension -ne ".unitypackage") {
        throw "The input file must be a .unitypackage file."
    }

    # Setup
    if (-not $OutputDir) {
        $OutputDir = [System.IO.Path]::GetDirectoryName($UnityPackagePath)
    }
    $tempDirPath = Join-Path -Path $OutputDir -ChildPath "temp"
    if (Test-Path -Path $tempDirPath) {
        Remove-Item -Path $tempDirPath -Recurse -Force
    }
    New-Item -ItemType Directory -Path $tempDirPath
    if (-not (Test-Path -Path $OutputDir)) {
        New-Item -ItemType Directory -Path $OutputDir
    }

    # .tar.gz
    tar -xzf $UnityPackagePath -C $tempDirPath

    # Mapping
    $mapping = @{}
    Get-ChildItem -Path $tempDirPath | ForEach-Object {
        $dirPath = $_.FullName
        $guid = $_.Name

        if (Test-Path -Path $dirPath -PathType Container) {
            $path = ''

            Get-ChildItem -Path $dirPath | ForEach-Object {
                if ($_.Name -eq "pathname") {
                    $path = Get-Content -Path $_.FullName -Encoding utf8 | Select-Object -First 1
                }
            }

            $mapping[$guid] = $path
        }
    }

    # File Move
    foreach ($guid in $mapping.Keys) {
        $path = Split-Path -Path $mapping[$guid] -Parent
        $fileName = Split-Path -Path $mapping[$guid] -Leaf
        $metaFileName = "${fileName}.meta"

        $destDir = Join-Path $OutputDir -ChildPath $path

        $assetFile = Join-Path $destDir -ChildPath $fileName
        $metaFile = Join-Path $destDir -ChildPath $metaFileName

        $source = Join-Path -Path $tempDirPath -ChildPath "${guid}/asset"
        $metaSource = Join-Path -Path $tempDirPath -ChildPath "${guid}/asset.meta"

        if (-not (Test-Path -Path $destDir)) {
            New-Item -ItemType Directory -Path $destDir -ErrorAction Stop | Out-Null
        }

        if (Test-Path -Path $source) {
            Move-Item -Path $source -Destination $assetFile -Force -ErrorAction Stop | Out-Null
        }
        if (Test-Path -Path $metaSource) {
            Move-Item -Path $metaSource -Destination $metaFile -Force -ErrorAction Stop | Out-Null
        }

        Write-Output "$guid => $($mapping[$guid])"
    }

    Remove-Item -Path $tempDirPath -Recurse -Force
}

<#
.Synopsis
    Convert to .unitypackage
.PARAMETER OutputFilePath
    The path where the .unitypackage file will be saved.
.PARAMETER TargetFiles
    The files to be included in the .unitypackage file. (Do not include .meta)
.EXAMPLE
    Compress-UnityPackage -OutputFilePath "C:\path\to\output.unitypackage" -TargetFiles "C:\path\to\Assets\MyAsset.prefab", "C:\path\to\Assets\MyScript.cs"
#>

function Compress-UnityPackage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$OutputFilePath,

        [Parameter(Mandatory = $true)]
        [string[]]$TargetFiles
    )

    $outputFileExtension = [System.IO.Path]::GetExtension($OutputFilePath)
    if ($outputFileExtension -ne ".unitypackage") {
        throw "The output file must be a .unitypackage file."
    }
    if ($TargetFiles.Count -eq 0) {
        throw "The target files are empty."
    }

    # NOTE: Exclude files with .meta extension
    $TargetFiles = $TargetFiles | Where-Object { [System.IO.Path]::GetExtension($_) -ne ".meta" }

    $outputDirPath = [System.IO.Path]::GetDirectoryName($OutputFilePath)
    $tempDirPath = Join-Path -Path $outputDirPath -ChildPath "temp"
    if (Test-Path -Path $tempDirPath) {
        Remove-Item -Path $tempDirPath -Recurse -Force
    }
    New-Item -ItemType Directory -Path $tempDirPath

    # NOTE: "preview.png" is not supported
    # "preview.png" is the thumbnail displayed on the Asset Store website.
    $mapping = @{}
    foreach ($targetFilePath in $TargetFiles) {
        # include file
        $targetFileMetaPath = "${targetFilePath}.meta"

        if (-not (Test-Path -Path $targetFilePath)) {
            throw "The target file does not exist: $targetFilePath"
        }
        if (-not (Test-Path -Path $targetFileMetaPath)) {
            throw "The target file's meta does not exist: $targetFileMetaPath"
        }

        $guid = ''
        Get-Content -Path $targetFileMetaPath -Encoding utf8 | ForEach-Object {
            if ($_ -match "guid: ([0-9a-fA-F]{32})") {
                $guid = $Matches[1]
            }
        }
        if (-not $guid) {
            throw "The target file's meta does not contain a guid: $targetFileMetaPath"
        }
        $mapping[$guid] = $targetFilePath

        # include directory
        $currentDirPath = Split-Path -Path $targetFilePath -Parent
        $currentDirName = Split-Path -Path $currentDirPath -Leaf
        while ($currentDirName -ne "Assets" -and $currentDirPath -ne [System.IO.Path]::GetPathRoot($currentDirPath)) {
            $parentDirPath = Split-Path -Path $currentDirPath -Parent
            $parentDirName = Split-Path -Path $parentDirPath -Leaf
            $metaPath = Join-Path -Path $parentDirPath -ChildPath "${currentDirName}.meta"
            
            if (Test-Path -Path $metaPath) {
                $dirGuid = ''
                Get-Content -Path $metaPath -Encoding utf8 | ForEach-Object {
                    if ($_ -match "guid: ([0-9a-fA-F]{32})") {
                        $dirGuid = $Matches[1]
                    }
                }
                if (-not $dirGuid) {
                    throw "The target file's meta does not contain a guid: $metaPath"
                }

                $mapping[$dirGuid] = $currentDirPath
            }
            $currentDirPath = $parentDirPath
            $currentDirName = $parentDirName
        }
    }

    foreach ($guid in $mapping.Keys) {
        $dirPath = Join-Path -Path $tempDirPath -ChildPath $guid
        New-Item -ItemType Directory -Path $dirPath -ErrorAction Stop | Out-Null

        $sourceFile = $mapping[$guid]
        $sourceMetaFile = "${sourceFile}.meta"

        $assetPath = Join-Path -Path $dirPath -ChildPath "asset"
        $metaPath = Join-Path -Path $dirPath -ChildPath "asset.meta"
        $pathnamePath = Join-Path -Path $dirPath -ChildPath "pathname"

        if (Test-Path -Path $sourceFile -PathType Leaf) {
            Copy-Item  -Path $sourceFile -Destination $assetPath -Force -ErrorAction Stop | Out-Null
        }
        Copy-Item  -Path $sourceMetaFile -Destination $metaPath -Force -ErrorAction Stop | Out-Null

        # NOTE: Extracts the first matching ‘Assets’ and subsequent parts from the Path.
        $path = $mapping[$guid] -replace '.*?(Assets)', 'Assets'
        Set-Content -Path $pathnamePath -Value $path -Encoding utf8 -ErrorAction Stop | Out-Null

        Write-Output "$($mapping[$guid]) => $guid"
    }

    tar -czf $OutputFilePath -C $tempDirPath .

    Remove-Item -Path $tempDirPath -Recurse -Force
}