diff --git a/.github/workflows/2023.2.20f1_build.yml b/.github/workflows/2023.2.20f1_build.yml deleted file mode 100644 index 9aa513e..0000000 --- a/.github/workflows/2023.2.20f1_build.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: 2023.2.20f1-Build - -on: - pull_request: - branches: - - main - push: - branches: - - main - -jobs: - editor-tests: - uses: ./.github/workflows/main.yml - with: - projectPath: './' - unityVersion: '2023.2.20f1' - testMode: 'standalone' - secrets: inherit \ No newline at end of file diff --git a/.github/workflows/2023.2.20f1_editor.yml b/.github/workflows/2023.2.20f1_editor.yml deleted file mode 100644 index 729c4ed..0000000 --- a/.github/workflows/2023.2.20f1_editor.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: 2023.2.20f1-Editor - -on: - pull_request: - branches: - - main - push: - branches: - - main - -jobs: - editor-tests: - uses: ./.github/workflows/main.yml - with: - projectPath: './' - unityVersion: '2023.2.20f1' - testMode: 'editmode' - secrets: inherit \ No newline at end of file diff --git a/.github/workflows/2023.2.20f1_player.yml b/.github/workflows/2023.2.20f1_player.yml deleted file mode 100644 index 43d7a3d..0000000 --- a/.github/workflows/2023.2.20f1_player.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: 2023.2.20f1-Player - -on: - pull_request: - branches: - - main - push: - branches: - - main - -jobs: - editor-tests: - uses: ./.github/workflows/main.yml - with: - projectPath: './' - unityVersion: '2023.2.20f1' - testMode: 'playmode' - secrets: inherit \ No newline at end of file diff --git a/.github/workflows/6000.0.37f1_build.yml b/.github/workflows/6000.0.37f1_build.yml deleted file mode 100644 index 194e002..0000000 --- a/.github/workflows/6000.0.37f1_build.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: 6000.0.37f1-Build - -on: - pull_request: - branches: - - main - push: - branches: - - main - -jobs: - editor-tests: - uses: ./.github/workflows/main.yml - with: - projectPath: './' - unityVersion: '6000.0.37f1' - testMode: 'standalone' - secrets: inherit \ No newline at end of file diff --git a/.github/workflows/6000.0.37f1_editor.yml b/.github/workflows/6000.0.37f1_editor.yml deleted file mode 100644 index 11994ab..0000000 --- a/.github/workflows/6000.0.37f1_editor.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: 6000.0.37f1-Editor - -on: - pull_request: - branches: - - main - push: - branches: - - main - -jobs: - editor-tests: - uses: ./.github/workflows/main.yml - with: - projectPath: './' - unityVersion: '6000.0.37f1' - testMode: 'editmode' - secrets: inherit \ No newline at end of file diff --git a/.github/workflows/6000.0.37f1_player.yml b/.github/workflows/6000.0.37f1_player.yml deleted file mode 100644 index 5573d53..0000000 --- a/.github/workflows/6000.0.37f1_player.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: 6000.0.37f1-Player - -on: - pull_request: - branches: - - main - push: - branches: - - main - -jobs: - editor-tests: - uses: ./.github/workflows/main.yml - with: - projectPath: './' - unityVersion: '6000.0.37f1' - testMode: 'playmode' - secrets: inherit \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 08e113f..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Editor Tests - -on: - workflow_call: - inputs: - projectPath: - required: true - type: string - unityVersion: - required: true - type: string - testMode: - required: true - type: string - secrets: - UNITY_LICENSE: - required: true - UNITY_EMAIL: - required: true - UNITY_PASSWORD: - required: true - -jobs: - test: - name: ${{ inputs.unityVersion }} ${{ inputs.testMode }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - lfs: false - - uses: actions/cache@v4 - with: - path: ${{ inputs.projectPath }}/Library - key: Library-${{ inputs.unityVersion }} - restore-keys: | - Library- - - uses: game-ci/unity-test-runner@v4 - id: tests - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - with: - projectPath: ${{ inputs.projectPath }} - unityVersion: ${{ inputs.unityVersion }} - testMode: ${{ inputs.testMode }} - artifactsPath: ${{ inputs.testMode }}-artifacts - githubToken: ${{ secrets.GITHUB_TOKEN }} - checkName: ${{ inputs.unityVersion }} ${{ inputs.testMode }} Test Results \ No newline at end of file diff --git a/.github/workflows/release.yml-sample b/.github/workflows/release.yml-sample new file mode 100644 index 0000000..46366cb --- /dev/null +++ b/.github/workflows/release.yml-sample @@ -0,0 +1,306 @@ +name: release + +on: + workflow_dispatch: + push: + branches: + - main + +env: + INSTALLER_UNITY_VERSION: 2022.3.61f1 + +jobs: + check-version-tag: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get_version.outputs.current-version }} + prev_tag: ${{ steps.prev_tag.outputs.tag }} + tag_exists: ${{ steps.tag_exists.outputs.exists }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Get version from package.json + id: get_version + uses: martinbeentjes/npm-get-version-action@v1.3.1 + with: + path: Unity-Package/Assets/root + + - name: Find previous version tag + id: prev_tag + uses: WyriHaximus/github-action-get-previous-tag@v1 + + - name: Check if tag exists + id: tag_exists + uses: mukunku/tag-exists-action@v1.6.0 + with: + tag: ${{ steps.get_version.outputs.current-version }} + + build-unity-installer: + runs-on: ubuntu-latest + needs: [check-version-tag] + if: needs.check-version-tag.outputs.tag_exists == 'false' + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Cache Unity Library + uses: actions/cache@v4 + with: + path: ./Installer/Library + key: Library-Unity-Installer + + - name: Test Unity Installer (EditMode) + uses: game-ci/unity-test-runner@v4 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + with: + projectPath: ./Installer + unityVersion: ${{ env.INSTALLER_UNITY_VERSION }} + customImage: "unityci/editor:ubuntu-${{ env.INSTALLER_UNITY_VERSION }}-base-3" + testMode: editmode + githubToken: ${{ secrets.GITHUB_TOKEN }} + checkName: Unity Installer EditMode Test Results + artifactsPath: artifacts-installer-editmode + customParameters: -CI true -GITHUB_ACTIONS true + + - name: Clean Unity artifacts and reset git state + run: | + # Force remove Unity generated files with restricted permissions + sudo rm -rf ./Installer/Logs/ || true + sudo rm -rf ./Installer/Temp/ || true + sudo rm -rf ./artifacts-installer-editmode/ || true + + # Reset only tracked files to their committed state + git reset --hard HEAD + echo "Cleaned Unity artifacts and reset tracked files" + + - name: Export Unity Package + uses: game-ci/unity-builder@v4 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + with: + projectPath: ./Installer + unityVersion: ${{ env.INSTALLER_UNITY_VERSION }} + customImage: "unityci/editor:ubuntu-${{ env.INSTALLER_UNITY_VERSION }}-base-3" + buildName: Unity-Installer + buildsPath: build + buildMethod: YOUR_PACKAGE_ID.Installer.PackageExporter.ExportPackage + customParameters: -CI true -GITHUB_ACTIONS true + + - name: Upload Unity Package as artifact + uses: actions/upload-artifact@v4 + with: + name: unity-installer-package + path: ./Installer/build/YOUR_PACKAGE_NAME_INSTALLER_FILE.unitypackage + + # --- UNITY TESTS --- + # ------------------- + + # --- EDIT MODE --- + + test-unity-2022-3-61f1-editmode: + needs: [build-unity-installer] + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2022.3.61f1" + testMode: "editmode" + secrets: inherit + + test-unity-2023-2-20f1-editmode: + needs: [build-unity-installer] + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2023.2.20f1" + testMode: "editmode" + secrets: inherit + + test-unity-6000-2-3f1-editmode: + needs: [build-unity-installer] + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "6000.2.3f1" + testMode: "editmode" + secrets: inherit + + # --- PLAY MODE --- + + test-unity-2022-3-61f1-playmode: + needs: [build-unity-installer] + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2022.3.61f1" + testMode: "playmode" + secrets: inherit + + test-unity-2023-2-20f1-playmode: + needs: [build-unity-installer] + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2023.2.20f1" + testMode: "playmode" + secrets: inherit + + test-unity-6000-2-3f1-playmode: + needs: [build-unity-installer] + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "6000.2.3f1" + testMode: "playmode" + secrets: inherit + + # --- STANDALONE --- + + test-unity-2022-3-61f1-standalone: + needs: [build-unity-installer] + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2022.3.61f1" + testMode: "standalone" + secrets: inherit + + test-unity-2023-2-20f1-standalone: + needs: [build-unity-installer] + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2023.2.20f1" + testMode: "standalone" + secrets: inherit + + test-unity-6000-2-3f1-standalone: + needs: [build-unity-installer] + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "6000.2.3f1" + testMode: "standalone" + secrets: inherit + + # ------------------- + + release-unity-plugin: + runs-on: ubuntu-latest + needs: + [ + check-version-tag, + build-unity-installer, + test-unity-2022-3-61f1-editmode, + test-unity-2022-3-61f1-playmode, + test-unity-2022-3-61f1-standalone, + test-unity-2023-2-20f1-editmode, + test-unity-2023-2-20f1-playmode, + test-unity-2023-2-20f1-standalone, + test-unity-6000-2-3f1-editmode, + test-unity-6000-2-3f1-playmode, + test-unity-6000-2-3f1-standalone, + ] + if: needs.check-version-tag.outputs.tag_exists == 'false' + outputs: + version: ${{ needs.check-version-tag.outputs.version }} + success: ${{ steps.rel_desc.outputs.success }} + release_notes: ${{ steps.rel_desc.outputs.release_body }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Generate release description + id: rel_desc + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + version=${{ needs.check-version-tag.outputs.version }} + prev_tag=${{ needs.check-version-tag.outputs.prev_tag }} + repo_url="https://github.com/${GITHUB_REPOSITORY}" + today=$(date +'%B %e, %Y') + + echo "repo_url: $repo_url" + echo "today: $today" + + echo "# Package $version" > release.md + echo "**Released:** *$today*" >> release.md + + echo "" >> release.md + echo "---" >> release.md + echo "" >> release.md + + if [ -n "$prev_tag" ]; then + echo "## Comparison" >> release.md + echo "See every change: [Compare $prev_tag...$version]($repo_url/compare/$prev_tag...$version)" >> release.md + + echo "" >> release.md + echo "---" >> release.md + echo "" >> release.md + + echo "## Commit Summary (Newest → Oldest)" >> release.md + for sha in $(git log --pretty=format:'%H' $prev_tag..HEAD); do + username=$(gh api repos/${GITHUB_REPOSITORY}/commits/$sha --jq '.author.login // .commit.author.name') + message=$(git log -1 --pretty=format:'%s' $sha) + short_sha=$(git log -1 --pretty=format:'%h' $sha) + echo "- [\`$short_sha\`]($repo_url/commit/$sha) — $message by @$username" >> release.md + done + fi + + printf "release_body<> $GITHUB_OUTPUT + echo "success=true" >> $GITHUB_OUTPUT + + - name: Create Tag and Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.check-version-tag.outputs.version }} + name: ${{ needs.check-version-tag.outputs.version }} + body: ${{ steps.rel_desc.outputs.release_body }} + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-unity-installer: + runs-on: ubuntu-latest + needs: release-unity-plugin + if: needs.release-unity-plugin.outputs.success == 'true' + steps: + - name: Download Unity Package artifact + uses: actions/download-artifact@v4 + with: + name: unity-installer-package + path: ./ + + - name: Upload Unity Package to Release + uses: softprops/action-gh-release@v2 + with: + files: ./Unity-Installer.unitypackage + tag_name: ${{ needs.release-unity-plugin.outputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Cleanup job to remove build artifacts after publishing + cleanup-artifacts: + runs-on: ubuntu-latest + needs: [publish-unity-installer] + if: always() + steps: + - name: Delete Unity Package artifacts + uses: geekyeggo/delete-artifact@v5 + with: + name: unity-installer-package + failOnError: false + continue-on-error: true diff --git a/.github/workflows/test_pull_request.yml-sample b/.github/workflows/test_pull_request.yml-sample new file mode 100644 index 0000000..477cda0 --- /dev/null +++ b/.github/workflows/test_pull_request.yml-sample @@ -0,0 +1,88 @@ +name: test-pull-request + +on: + workflow_dispatch: + pull_request: + branches: [main, dev] + types: [opened, synchronize, reopened] + +jobs: + # --- EDIT MODE --- + + test-unity-2022-3-61f1-editmode: + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2022.3.61f1" + testMode: "editmode" + secrets: inherit + + test-unity-2023-2-20f1-editmode: + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2023.2.20f1" + testMode: "editmode" + secrets: inherit + + test-unity-6000-2-3f1-editmode: + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "6000.2.3f1" + testMode: "editmode" + secrets: inherit + + # --- PLAY MODE --- + + test-unity-2022-3-61f1-playmode: + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2022.3.61f1" + testMode: "playmode" + secrets: inherit + + test-unity-2023-2-20f1-playmode: + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2023.2.20f1" + testMode: "playmode" + secrets: inherit + + test-unity-6000-2-3f1-playmode: + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "6000.2.3f1" + testMode: "playmode" + secrets: inherit + + # --- STANDALONE --- + + test-unity-2022-3-61f1-standalone: + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2022.3.61f1" + testMode: "standalone" + secrets: inherit + + test-unity-2023-2-20f1-standalone: + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "2023.2.20f1" + testMode: "standalone" + secrets: inherit + + test-unity-6000-2-3f1-standalone: + uses: ./.github/workflows/test_unity_plugin.yml + with: + projectPath: "./Unity-Package" + unityVersion: "6000.2.3f1" + testMode: "standalone" + secrets: inherit + + # ------------------- diff --git a/.github/workflows/test_unity_plugin.yml b/.github/workflows/test_unity_plugin.yml new file mode 100644 index 0000000..6d0e74b --- /dev/null +++ b/.github/workflows/test_unity_plugin.yml @@ -0,0 +1,95 @@ +name: test-unity-plugin + +############################################################################## +# 1. Triggers +############################################################################## +on: + workflow_dispatch: + workflow_call: + inputs: + projectPath: { required: true, type: string } + unityVersion: { required: true, type: string } + testMode: { required: true, type: string } + secrets: + UNITY_LICENSE: { required: true } + UNITY_EMAIL: { required: true } + UNITY_PASSWORD: { required: true } + +############################################################################## +# 2. Job – runs only after a maintainer applies the `ci-ok` label +############################################################################## +jobs: + test: + if: | + github.event_name != 'pull_request_target' || + contains(github.event.pull_request.labels.*.name,'ci-ok') + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + platform: [base, windows-mono] + + name: ${{ inputs.unityVersion }} ${{ inputs.testMode }} on ${{ matrix.platform }} + runs-on: ${{ matrix.os }} + + # permissions: # minimize the default token + # contents: write + # pull-requests: write + + steps: + # --------------------------------------------------------------------- # + # 2-a. (PR only) abort if the contributor also changed workflow files + # --------------------------------------------------------------------- # + - name: Abort if workflow files modified + if: ${{ github.event_name == 'pull_request_target' }} + run: | + git fetch --depth=1 origin "${{ github.base_ref }}" + if git diff --name-only HEAD origin/${{ github.base_ref }} | grep -q '^\.github/workflows/'; then + echo "::error::This PR edits workflow files – refusing to run with secrets"; exit 1; + fi + + # --------------------------------------------------------------------- # + # 2-b. Checkout the contributor’s commit safely + # --------------------------------------------------------------------- # + - uses: actions/checkout@v6 + with: + lfs: false + + # --------------------------------------------------------------------- # + # 2-c. Cache & run the Unity test-runner + # --------------------------------------------------------------------- # + - uses: actions/cache@v4 + with: + path: | + ${{ inputs.projectPath }}/Library + ~/.cache/unity3d + key: Library-${{ inputs.unityVersion }}-${{ inputs.testMode }}-${{ matrix.platform }} + + # --------------------------------------------------------------------- # + - name: Generate custom image name + id: custom_image + run: echo "image=unityci/editor:ubuntu-${{ inputs.unityVersion }}-${{ matrix.platform }}-3" >> $GITHUB_OUTPUT + shell: bash + + - uses: game-ci/unity-test-runner@v4 + id: tests + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + with: + projectPath: ${{ inputs.projectPath }} + unityVersion: ${{ inputs.unityVersion }} + testMode: ${{ inputs.testMode }} + customImage: ${{ steps.custom_image.outputs.image }} + githubToken: ${{ secrets.GITHUB_TOKEN }} + checkName: ${{ inputs.unityVersion }} ${{ inputs.testMode }} ${{ matrix.platform }} Test Results + artifactsPath: artifacts-${{ inputs.unityVersion }}-${{ inputs.testMode }}-${{ matrix.platform }} + customParameters: -CI true -GITHUB_ACTIONS true + + # --------------------------------------------------------------------- # + - uses: actions/upload-artifact@v4 + if: always() + with: + name: Test results for ${{ inputs.unityVersion }} ${{ inputs.testMode }} on ${{ matrix.platform }} + path: ${{ steps.tests.outputs.artifactsPath }} diff --git a/.gitignore b/.gitignore index 1caa1ed..37581ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,75 +1,2 @@ -# This .gitignore file should be placed at the root of your Unity project directory -# -# Get latest from https://github.com/github/gitignore/blob/main/Docs/Unity.gitignore -# -/[Ll]ibrary/ -/[Tt]emp/ -/[Oo]bj/ -/[Bb]uild/ -/[Bb]uilds/ -/[Ll]ogs/ -/[Mm]emoryCaptures/ - -# Asset meta data should only be ignored when the corresponding asset is also ignored -!/[Aa]ssets/**/*.meta - -# Uncomment this line if you wish to ignore the asset store tools plugin -# /[Aa]ssets/AssetStoreTools* - -# Autogenerated Jetbrains Rider plugin -[Aa]ssets/Plugins/Editor/JetBrains* - -# Visual Studio cache directory -.vs/ - -# Gradle cache directory -.gradle/ - -# Autogenerated VS/MD/Consulo solution and project files -ExportedObj/ -.consulo/ -*.csproj -*.unityproj -*.sln -*.suo -*.tmp -*.user -*.userprefs -*.pidb -*.booproj -*.svd -*.pdb -*.mdb -*.opendb -*.VC.db - -# Unity3D generated meta files -*.pidb.meta -*.pdb.meta -*.mdb.meta - -# Unity3D generated file on crash reports -sysinfo.txt - -# Builds -*.apk -*.unitypackage - -# Crashlytics generated file -crashlytics-build.properties - -*/AndroidLogcatSettings.asset -/Assets/StreamingAssets - -# Crashlytics generated file -crashlytics-build.properties - -# ODIN Ignore the auto-generated AOT compatibility dll. -/Assets/Plugins/Sirenix/Assemblies/AOT/* -/Assets/Plugins/Sirenix/Assemblies/AOT** - -# ODIN Ignore all unpacked demos. -/Assets/Plugins/Sirenix/Demos/* - -# ODIN plugin -/Assets/Plugins/Sirenix** +# Claude +.claude \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6cfac07 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "editmode", + "playmode" + ] +} \ No newline at end of file diff --git a/Commands/bump-version.ps1 b/Commands/bump-version.ps1 new file mode 100644 index 0000000..2d5ae6c --- /dev/null +++ b/Commands/bump-version.ps1 @@ -0,0 +1,206 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Automated version bumping script for Unity Package project + +.DESCRIPTION + Updates version numbers across all project files automatically to prevent human errors. + Supports preview mode for safe testing. + +.PARAMETER NewVersion + The new version number in semver format (e.g., "0.18.0") + +.PARAMETER WhatIf + Preview changes without applying them + +.EXAMPLE + .\bump-version.ps1 -NewVersion "0.18.0" + +.EXAMPLE + .\bump-version.ps1 -NewVersion "0.18.0" -WhatIf +#> + +param( + [Parameter(Mandatory = $true)] + [string]$NewVersion, + + [switch]$WhatIf +) + +# Set location to repository root (parent of commands folder) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptDir +Push-Location $repoRoot + +# Script configuration +$ErrorActionPreference = "Stop" + +# Version file locations (relative to script root) +$VersionFiles = @( + @{ + Path = "Unity-Package/Assets/root/package.json" + Pattern = '"version":\s*"[\d\.]+"' + Replace = '"version": "{VERSION}"' + Description = "Unity package version" + }, + @{ + Path = "Installer/Assets/YOUR_PACKAGE_NAME_INSTALLER/Installer.cs" + Pattern = 'public const string Version = "[\d\.]+";' + Replace = 'public const string Version = "{VERSION}";' + Description = "Installer C# version constant" + } +) + +function Write-ColorText { + param([string]$Text, [string]$Color = "White") + Write-Host $Text -ForegroundColor $Color +} + +function Test-SemanticVersion { + param([string]$Version) + + if ([string]::IsNullOrWhiteSpace($Version)) { + return $false + } + + # Basic semver pattern: major.minor.patch (with optional prerelease/build) + $pattern = '^\d+\.\d+\.\d+(-[a-zA-Z0-9\-\.]+)?(\+[a-zA-Z0-9\-\.]+)?$' + return $Version -match $pattern +} + +function Get-CurrentVersion { + # Extract current version from package.json + $packageJsonPath = "Unity-Package/Assets/root/package.json" + if (-not (Test-Path $packageJsonPath)) { + throw "Could not find package.json at: $packageJsonPath" + } + + $content = Get-Content $packageJsonPath -Raw + if ($content -match '"version":\s*"([\d\.]+)"') { + return $Matches[1] + } + + throw "Could not extract current version from package.json" +} + +function Update-VersionFiles { + param([string]$OldVersion, [string]$NewVersion, [bool]$PreviewOnly = $false) + + $changes = @() + + foreach ($file in $VersionFiles) { + $fullPath = $file.Path + + if (-not (Test-Path $fullPath)) { + Write-ColorText "⚠️ File not found: $($file.Path)" "Yellow" + continue + } + + $content = Get-Content $fullPath -Raw + $originalContent = $content + + # Create the replacement string + $replacement = $file.Replace -replace '\{VERSION\}', $NewVersion + + # Apply the replacement + $newContent = $content -replace $file.Pattern, $replacement + + # Check if any changes were made + if ($originalContent -ne $newContent) { + # Count matches for reporting + $regexMatches = [regex]::Matches($originalContent, $file.Pattern) + + $changes += @{ + Path = $file.Path + Description = $file.Description + Matches = $regexMatches.Count + Content = $newContent + OriginalContent = $originalContent + } + + Write-ColorText "📝 $($file.Description): $($regexMatches.Count) occurrence(s)" "Green" + + # Show the actual changes + foreach ($match in $regexMatches) { + $newValue = $match.Value -replace $file.Pattern, $replacement + Write-ColorText " $($match.Value) → $newValue" "Gray" + } + } + else { + Write-ColorText "⚠️ No matches found in: $($file.Path)" "Yellow" + Write-ColorText " Pattern: $($file.Pattern)" "Gray" + } + } + + if ($changes.Count -eq 0) { + Write-ColorText "❌ No version references found to update!" "Red" + Pop-Location + exit 1 + } + + if ($PreviewOnly) { + Write-ColorText "`n📋 Preview Summary:" "Cyan" + Write-ColorText "Files to be modified: $($changes.Count)" "White" + Write-ColorText "Total replacements: $(($changes | Measure-Object -Property Matches -Sum).Sum)" "White" + return $null + } + + # Apply changes + foreach ($change in $changes) { + $fullPath = $change.Path + Set-Content -Path $fullPath -Value $change.Content -NoNewline + } + + return $changes +} + +# Main execution +try { + Write-ColorText "🚀 Package Version Bump Script" "Cyan" + Write-ColorText "=================================" "Cyan" + + # Validate semantic version format + if (-not (Test-SemanticVersion $NewVersion)) { + Write-ColorText "❌ Invalid semantic version format: $NewVersion" "Red" + Write-ColorText "Expected format: major.minor.patch (e.g., '1.2.3')" "Yellow" + Pop-Location + exit 1 + } + + # Get current version + $currentVersion = Get-CurrentVersion + Write-ColorText "📋 Current version: $currentVersion" "White" + Write-ColorText "📋 New version: $NewVersion" "White" + + if ($currentVersion -eq $NewVersion) { + Write-ColorText "⚠️ New version is the same as current version" "Yellow" + Pop-Location + exit 0 + } + + Write-ColorText "`n🔍 Scanning for version references..." "Cyan" + + # Update version files + $changes = Update-VersionFiles -OldVersion $currentVersion -NewVersion $NewVersion -PreviewOnly $WhatIf + + if ($WhatIf) { + Write-ColorText "`n✅ Preview completed. Use without -WhatIf to apply changes." "Green" + Pop-Location + exit 0 + } + + if ($changes -and $changes.Count -gt 0) { + Write-ColorText "`n🎉 Version bump completed successfully!" "Green" + Write-ColorText " Updated $($changes.Count) files" "White" + Write-ColorText " Total replacements: $(($changes | Measure-Object -Property Matches -Sum).Sum)" "White" + Write-ColorText " Version: $currentVersion → $NewVersion" "White" + Write-ColorText "`n💡 Remember to commit these changes to git" "Cyan" + } + + Pop-Location +} +catch { + Write-ColorText "`n❌ Script failed: $($_.Exception.Message)" "Red" + Pop-Location + exit 1 +} \ No newline at end of file diff --git a/Commands/clean_template.bat b/Commands/clean_template.bat deleted file mode 100644 index 5094ebe..0000000 --- a/Commands/clean_template.bat +++ /dev/null @@ -1,13 +0,0 @@ -cd .. -rmdir /s /q "ProjectSettings" -rmdir /s /q "Packages" -rmdir /s /q ".vscode" - -for /r %%f in (*.meta) do ( - del /f /q "%%f" -) - -del /f /q "Unity-Package-Template.sln" - -echo Done! -pause \ No newline at end of file diff --git a/Commands/copy_readme.bat b/Commands/copy_readme.bat deleted file mode 100644 index d1097fe..0000000 --- a/Commands/copy_readme.bat +++ /dev/null @@ -1,2 +0,0 @@ -xcopy ..\README.md ..\Assets\root\README.md* /Y -xcopy ..\README.md ..\Assets\root\Documentation~\README.md* /Y \ No newline at end of file diff --git a/Commands/git_push_to_upm_branch.bat b/Commands/git_push_to_upm_branch.bat deleted file mode 100644 index bc039b8..0000000 --- a/Commands/git_push_to_upm_branch.bat +++ /dev/null @@ -1,3 +0,0 @@ -cd .. -git subtree push --prefix Assets/root origin upm -pause \ No newline at end of file diff --git a/Commands/git_push_to_upm_branch.makefile b/Commands/git_push_to_upm_branch.makefile deleted file mode 100644 index 526152e..0000000 --- a/Commands/git_push_to_upm_branch.makefile +++ /dev/null @@ -1,3 +0,0 @@ -deploy: - cd .. - git subtree push --prefix Assets/root origin upm \ No newline at end of file diff --git a/Commands/github_draft_release.bat b/Commands/github_draft_release.bat deleted file mode 100644 index 2f01bf8..0000000 --- a/Commands/github_draft_release.bat +++ /dev/null @@ -1,23 +0,0 @@ -cd ..\Assets\root -@echo off -echo ---------------------------------------------------- -echo Executing "npm pkg get version" -FOR /F "tokens=* USEBACKQ" %%F IN (`npm pkg get version`) DO ( -SET RawVersion=%%F -) -echo Version of current package is extracted: %RawVersion% -SET CleanVersion=%RawVersion:~1,-1% -echo Current version: %CleanVersion% - -git push -u origin HEAD - -echo ---------------------------------------------------- -cd ..\..\ -echo Creating GitHub release with tag=%CleanVersion% -@echo on -gh release create %CleanVersion% --draft --generate-notes --title %CleanVersion% -gh repo view --web -@echo off -echo ---------------------------------------------------- - -pause diff --git a/Commands/github_release.bat b/Commands/github_release.bat deleted file mode 100644 index 68d91dc..0000000 --- a/Commands/github_release.bat +++ /dev/null @@ -1,23 +0,0 @@ -cd ..\Assets\root -@echo off -echo ---------------------------------------------------- -echo Executing "npm pkg get version" -FOR /F "tokens=* USEBACKQ" %%F IN (`npm pkg get version`) DO ( -SET RawVersion=%%F -) -echo Version of current package is extracted: %RawVersion% -SET CleanVersion=%RawVersion:~1,-1% -echo Current version: %CleanVersion% - -git push -u origin HEAD - -echo ---------------------------------------------------- -cd ..\..\ -echo Creating GitHub release with tag=%CleanVersion% -@echo on -gh release create %CleanVersion% --generate-notes --title %CleanVersion% -gh release view %CleanVersion% --web -@echo off -echo ---------------------------------------------------- - -pause diff --git a/Commands/init.ps1 b/Commands/init.ps1 new file mode 100644 index 0000000..0e92d2a --- /dev/null +++ b/Commands/init.ps1 @@ -0,0 +1,135 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Initializes the Unity Package project by replacing placeholders. + +.DESCRIPTION + Replaces placeholders in file content, filenames, and directory names. + Placeholders: + - YOUR_PACKAGE_ID + - YOUR_PACKAGE_ID_LOWERCASE + - YOUR_PACKAGE_NAME + - YOUR_PACKAGE_NAME_INSTALLER + - YOUR_PACKAGE_NAME_INSTALLER_FILE + +.PARAMETER PackageId + The package ID (e.g., "com.company.package"). + +.PARAMETER PackageName + The package name (e.g., "My Package"). + +.EXAMPLE + .\init.ps1 -PackageId "com.mycompany.coolpackage" -PackageName "Cool Package" +#> + +param( + [Parameter(Mandatory = $true)] + [string]$PackageId, + + [Parameter(Mandatory = $true)] + [string]$PackageName +) + +$ErrorActionPreference = "Stop" + +# Derived variables +$PackageIdLowercase = $PackageId.ToLower() +$PackageNameInstaller = "$PackageName Installer" +$PackageNameInstallerFile = $PackageNameInstaller -replace ' ', '-' + +# Replacements map +$Replacements = @{ + "YOUR_PACKAGE_ID" = $PackageId + "YOUR_PACKAGE_ID_LOWERCASE" = $PackageIdLowercase + "YOUR_PACKAGE_NAME" = $PackageName + "YOUR_PACKAGE_NAME_INSTALLER_FILE" = $PackageNameInstallerFile + "YOUR_PACKAGE_NAME_INSTALLER" = $PackageNameInstaller +} + +# Sort keys by length descending to avoid partial replacements +$SortedKeys = $Replacements.Keys | Sort-Object { $_.Length } -Descending + +# Root directory (parent of commands) +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = Split-Path -Parent $ScriptDir + +Write-Host "Initializing package with:" -ForegroundColor Cyan +Write-Host " ID: $PackageId" +Write-Host " Name: $PackageName" +Write-Host " Installer: $PackageNameInstaller" +Write-Host " Installer File: $PackageNameInstallerFile" +Write-Host "" + +# Define target paths +$TargetPaths = @("Installer", "Unity-Package", "README.md") + +# 1. Replace content in files +Write-Host "Replacing content in files..." -ForegroundColor Yellow +$Files = @() +foreach ($Path in $TargetPaths) { + $FullPath = Join-Path $RepoRoot $Path + if (Test-Path $FullPath) { + if ((Get-Item $FullPath).PSIsContainer) { + $Files += Get-ChildItem -Path $FullPath -Recurse -File + } + else { + $Files += Get-Item $FullPath + } + } + else { + Write-Warning "Path not found: $FullPath" + } +} + +foreach ($File in $Files) { + $Content = Get-Content -Path $File.FullName -Raw + $NewContent = $Content + $Modified = $false + + foreach ($Key in $SortedKeys) { + if ($NewContent -match $Key) { + $NewContent = $NewContent -replace $Key, $Replacements[$Key] + $Modified = $true + } + } + + if ($Modified) { + Set-Content -Path $File.FullName -Value $NewContent -NoNewline + Write-Host " Updated: $($File.FullName)" -ForegroundColor Gray + } +} + +# 2. Rename files and directories +# We need to do this depth-first (bottom-up) so we don't rename a parent directory before its children +Write-Host "Renaming files and directories..." -ForegroundColor Yellow +$Items = @() +foreach ($Path in $TargetPaths) { + $FullPath = Join-Path $RepoRoot $Path + if (Test-Path $FullPath) { + if ((Get-Item $FullPath).PSIsContainer) { + $Items += Get-ChildItem -Path $FullPath -Recurse + } + else { + $Items += Get-Item $FullPath + } + } +} + +# Sort depth-first (longest path first) to handle nested renames correctly +$Items = $Items | Sort-Object -Property FullName -Descending | Select-Object -Unique + +foreach ($Item in $Items) { + $NewName = $Item.Name + foreach ($Key in $SortedKeys) { + if ($NewName -match $Key) { + $NewName = $NewName -replace $Key, $Replacements[$Key] + } + } + + if ($NewName -ne $Item.Name) { + Rename-Item -Path $Item.FullName -NewName $NewName + Write-Host " Renamed: $($Item.Name) -> $NewName" -ForegroundColor Gray + } +} + +Write-Host "Done!" -ForegroundColor Green diff --git a/Commands/npm_add_user.bat b/Commands/npm_add_user.bat deleted file mode 100644 index caa4682..0000000 --- a/Commands/npm_add_user.bat +++ /dev/null @@ -1,2 +0,0 @@ -cd ..\Assets\root -npm adduser \ No newline at end of file diff --git a/Commands/npm_deploy.bat b/Commands/npm_deploy.bat deleted file mode 100644 index 073183e..0000000 --- a/Commands/npm_deploy.bat +++ /dev/null @@ -1,5 +0,0 @@ -xcopy ..\README.md ..\Assets\root\README.md* /Y -xcopy ..\README.md ..\Assets\root\Documentation~\README.md* /Y -cd ..\Assets\root -npm publish -pause \ No newline at end of file diff --git a/Commands/npm_update_dependencies.bat b/Commands/npm_update_dependencies.bat deleted file mode 100644 index 4750512..0000000 --- a/Commands/npm_update_dependencies.bat +++ /dev/null @@ -1,3 +0,0 @@ -call npm --prefix ..\Assets\root update -del /f ..\Assets\root\package-lock.* -pause \ No newline at end of file diff --git a/Commands/npm_version_major.bat b/Commands/npm_version_major.bat deleted file mode 100644 index efc5e4f..0000000 --- a/Commands/npm_version_major.bat +++ /dev/null @@ -1,3 +0,0 @@ -cd ..\Assets\root -npm version major -pause \ No newline at end of file diff --git a/Commands/npm_version_minor.bat b/Commands/npm_version_minor.bat deleted file mode 100644 index 9b67308..0000000 --- a/Commands/npm_version_minor.bat +++ /dev/null @@ -1,3 +0,0 @@ -cd ..\Assets\root -npm version minor -pause \ No newline at end of file diff --git a/Commands/npm_version_patch.bat b/Commands/npm_version_patch.bat deleted file mode 100644 index 1438d96..0000000 --- a/Commands/npm_version_patch.bat +++ /dev/null @@ -1,3 +0,0 @@ -cd ..\Assets\root -npm version patch -pause \ No newline at end of file diff --git a/Commands/package_rename.bat b/Commands/package_rename.bat deleted file mode 100644 index c9cc089..0000000 --- a/Commands/package_rename.bat +++ /dev/null @@ -1,69 +0,0 @@ -@echo off -echo Started package rename process. - -echo Change to the target directory -cd ..\Assets\root || ( - echo Failed to change directory to ..\Assets\root - exit /b 1 -) - -echo Username: %~1 -echo Package Name: %~2 - -echo Check if the correct number of arguments is provided -if "%~2"=="" ( - echo Usage: package_rename.bat {username} {packageName} - exit /b 1 -) - -echo Debugging: Reading arguments -set "username=%~1" -set "packageName=%~2" - -echo Debugging: Converting to lowercase -for /f "usebackq delims=" %%A in (`powershell -Command "$env:packageName.ToLower()"`) do set "lowerPackageName=%%A" -for /f "usebackq delims=" %%A in (`powershell -Command "$env:username.ToLower()"`) do set "lowerUsername=%%A" - -echo Debugging: SetLocal -setlocal enabledelayedexpansion - -REM Debugging: Print variables -echo Username: %username% -echo PackageName: %packageName% -echo Lowercase PackageName: !lowerPackageName! -echo Lowercase Username: !lowerUsername! - -REM Recursively replace "package" and "Package" in file content, ignoring .md and .meta files -for /r %%F in (*) do ( - REM Check if the file ends with .md or .meta - if /i "%%~xF"==".asmdef" ( - REM Process the file - echo Processing file: %%F - powershell -Command "(Get-Content -Raw -Path '%%F') -replace 'Package', '%packageName%' -replace 'package', '!lowerPackageName!' -replace 'Your_Name', '%username%' -replace 'your_name', '!lowerUsername!' | Set-Content -Path '%%F'" || ( - echo Failed to process file %%F - ) - ) - if /i "%%~nxF"=="package.json" ( - REM Process the file - echo Processing file: %%F - powershell -Command "(Get-Content -Raw -Path '%%F') -replace 'Package', '%packageName%' -replace 'package', '!lowerPackageName!' -replace 'Your_Name', '%username%' -replace 'your_name', '!lowerUsername!' | Set-Content -Path '%%F'" || ( - echo Failed to process file %%F - ) - ) -) - -REM Recursively rename files with "package" and "Package" in their names, ignoring .md files -for /r %%F in (*) do ( - if /i "%%~xF"==".asmdef" ( - set "newName=%%~nxF" - set "newName=!newName:Package=%packageName%!" - if not "%%~nxF"=="!newName!" ( - ren "%%F" "!newName!" || ( - echo Failed to rename file %%F - ) - ) - ) -) - -echo Done! -pause \ No newline at end of file diff --git a/Installer/.gitignore b/Installer/.gitignore new file mode 100644 index 0000000..1b0a9ff --- /dev/null +++ b/Installer/.gitignore @@ -0,0 +1,78 @@ +# This .gitignore file should be placed at the root of your Unity project directory +# +# Get latest from https://github.com/github/gitignore/blob/main/Docs/Unity.gitignore +# +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Ll]ogs/ +/[Mm]emoryCaptures/ + +# Asset meta data should only be ignored when the corresponding asset is also ignored +!/[Aa]ssets/**/*.meta + +# Uncomment this line if you wish to ignore the asset store tools plugin +# /[Aa]ssets/AssetStoreTools* + +# Autogenerated Jetbrains Rider plugin +[Aa]ssets/Plugins/Editor/JetBrains* + +# Visual Studio cache directory +.vs/ + +# Gradle cache directory +.gradle/ + +# Autogenerated VS/MD/Consulo solution and project files +ExportedObj/ +.consulo/ +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db + +# Unity3D generated meta files +*.pidb.meta +*.pdb.meta +*.mdb.meta + +# Unity3D generated file on crash reports +sysinfo.txt + +# Builds +*.apk +*.unitypackage + +# Crashlytics generated file +crashlytics-build.properties + +*/AndroidLogcatSettings.asset +/Assets/StreamingAssets + +# Crashlytics generated file +crashlytics-build.properties + +# ODIN Ignore the auto-generated AOT compatibility dll. +/Assets/Plugins/Sirenix/Assemblies/AOT/* +/Assets/Plugins/Sirenix/Assemblies/AOT** + +# ODIN Ignore all unpacked demos. +/Assets/Plugins/Sirenix/Demos/* + +# ODIN plugin +/Assets/Plugins/Sirenix** + +# Claude +.claude \ No newline at end of file diff --git a/Installer/.vscode/extensions.json b/Installer/.vscode/extensions.json new file mode 100644 index 0000000..ddb6ff8 --- /dev/null +++ b/Installer/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "visualstudiotoolsforunity.vstuc" + ] +} diff --git a/Installer/.vscode/launch.json b/Installer/.vscode/launch.json new file mode 100644 index 0000000..da60e25 --- /dev/null +++ b/Installer/.vscode/launch.json @@ -0,0 +1,10 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Unity", + "type": "vstuc", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/Installer/.vscode/settings.json b/Installer/.vscode/settings.json new file mode 100644 index 0000000..7294c29 --- /dev/null +++ b/Installer/.vscode/settings.json @@ -0,0 +1,73 @@ +{ + "files.exclude": { + "**/.DS_Store": true, + "**/.git": true, + "**/.vs": true, + "**/.gitmodules": true, + "**/.vsconfig": true, + "**/*.booproj": true, + "**/*.pidb": true, + "**/*.suo": true, + "**/*.user": true, + "**/*.userprefs": true, + "**/*.unityproj": true, + "**/*.dll": true, + "**/*.exe": true, + "**/*.pdf": true, + "**/*.mid": true, + "**/*.midi": true, + "**/*.wav": true, + "**/*.gif": true, + "**/*.ico": true, + "**/*.jpg": true, + "**/*.jpeg": true, + "**/*.png": true, + "**/*.psd": true, + "**/*.tga": true, + "**/*.tif": true, + "**/*.tiff": true, + "**/*.3ds": true, + "**/*.3DS": true, + "**/*.fbx": true, + "**/*.FBX": true, + "**/*.lxo": true, + "**/*.LXO": true, + "**/*.ma": true, + "**/*.MA": true, + "**/*.obj": true, + "**/*.OBJ": true, + "**/*.asset": true, + "**/*.cubemap": true, + "**/*.flare": true, + "**/*.mat": true, + "**/*.meta": true, + "**/*.prefab": true, + "**/*.unity": true, + "build/": true, + "Build/": true, + "Library/": true, + "library/": true, + "obj/": true, + "Obj/": true, + "Logs/": true, + "logs/": true, + "ProjectSettings/": true, + "UserSettings/": true, + "temp/": true, + "Temp/": true + }, + "files.associations": { + "*.asset": "yaml", + "*.meta": "yaml", + "*.prefab": "yaml", + "*.unity": "yaml", + }, + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*.sln": "*.csproj", + }, + "dotnet.defaultSolution": "Installer.sln", + "cSpell.words": [ + "ivanmurzak" + ] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Installer.Manifest.cs b/Installer/Assets/YOUR_PACKAGE_TITLE/Installer.Manifest.cs new file mode 100644 index 0000000..49acd9f --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Installer.Manifest.cs @@ -0,0 +1,167 @@ +/* +┌────────────────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-Package-Template) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the MIT License. │ +│ See the LICENSE file in the project root for more information. │ +└────────────────────────────────────────────────────────────────────────────┘ +*/ +#nullable enable +using System.IO; +using System.Linq; +using UnityEngine; +using YOUR_PACKAGE_ID.Installer.SimpleJSON; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("YOUR_PACKAGE_ID.Installer.Tests")] +namespace YOUR_PACKAGE_ID.Installer +{ + public static partial class Installer + { + static string ManifestPath => Path.Combine(Application.dataPath, "../Packages/manifest.json"); + + // Property names + public const string Dependencies = "dependencies"; + public const string ScopedRegistries = "scopedRegistries"; + public const string Name = "name"; + public const string Url = "url"; + public const string Scopes = "scopes"; + + // Property values + public const string RegistryName = "package.openupm.com"; + public const string RegistryUrl = "https://package.openupm.com"; + public static readonly string[] PackageIds = new string[] { + "com.ivanmurzak", // Ivan Murzak's OpenUPM packages + "extensions.unity", // Ivan Murzak's OpenUPM packages (older) + "org.nuget.com.ivanmurzak", // Ivan Murzak's NuGet packages + "org.nuget.microsoft", // Microsoft NuGet packages + "org.nuget.system", // Microsoft NuGet packages + "org.nuget.r3" // R3 package NuGet package + }; + + /// + /// Determines if the version should be updated. Only update if installer version is higher than current version. + /// + /// Current package version string + /// Installer version string + /// True if version should be updated (installer version is higher), false otherwise + + internal static bool ShouldUpdateVersion(string currentVersion, string installerVersion) + { + if (string.IsNullOrEmpty(currentVersion)) + return true; // No current version, should install + + if (string.IsNullOrEmpty(installerVersion)) + return false; // No installer version, don't change + + try + { + // Try to parse as System.Version (semantic versioning) + var current = new System.Version(currentVersion); + var installer = new System.Version(installerVersion); + + // Only update if installer version is higher than current version + return installer > current; + } + catch (System.Exception) + { + Debug.LogWarning($"Failed to parse versions '{currentVersion}' or '{installerVersion}' as System.Version."); + // If version parsing fails, fall back to string comparison + // This ensures we don't break if version format is unexpected + return string.Compare(installerVersion, currentVersion, System.StringComparison.OrdinalIgnoreCase) > 0; + } + } + + public static void AddScopedRegistryIfNeeded(string manifestPath, int indent = 2) + { + if (!File.Exists(manifestPath)) + { + Debug.LogError($"{manifestPath} not found!"); + return; + } + var jsonText = File.ReadAllText(manifestPath) + .Replace("{ }", "{\n}") + .Replace("{}", "{\n}") + .Replace("[ ]", "[\n]") + .Replace("[]", "[\n]"); + + var manifestJson = JSONObject.Parse(jsonText); + if (manifestJson == null) + { + Debug.LogError($"Failed to parse {manifestPath} as JSON."); + return; + } + + var modified = false; + + // --- Add scoped registries if needed + var scopedRegistries = manifestJson[ScopedRegistries]; + if (scopedRegistries == null) + { + manifestJson[ScopedRegistries] = new JSONArray(); + modified = true; + } + + // --- Add OpenUPM registry if needed + var openUpmRegistry = scopedRegistries!.Linq + .Select(kvp => kvp.Value) + .Where(r => r.Linq + .Any(p => p.Key == Name && p.Value == RegistryName)) + .FirstOrDefault(); + + if (openUpmRegistry == null) + { + scopedRegistries.Add(openUpmRegistry = new JSONObject + { + [Name] = RegistryName, + [Url] = RegistryUrl, + [Scopes] = new JSONArray() + }); + modified = true; + } + + // --- Add missing scopes + var scopes = openUpmRegistry[Scopes]; + if (scopes == null) + { + openUpmRegistry[Scopes] = scopes = new JSONArray(); + modified = true; + } + foreach (var packageId in PackageIds) + { + var existingScope = scopes!.Linq + .Select(kvp => kvp.Value) + .Where(value => value == packageId) + .FirstOrDefault(); + if (existingScope == null) + { + scopes.Add(packageId); + modified = true; + } + } + + // --- Package Dependency (Version-aware installation) + // Only update version if installer version is higher than current version + // This prevents downgrades when users manually update to newer versions + var dependencies = manifestJson[Dependencies]; + if (dependencies == null) + { + manifestJson[Dependencies] = dependencies = new JSONObject(); + modified = true; + } + + // Only update version if installer version is higher than current version + var currentVersion = dependencies[PackageId]; + if (currentVersion == null || ShouldUpdateVersion(currentVersion, Version)) + { + dependencies[PackageId] = Version; + modified = true; + } + + // --- Write changes back to manifest + if (modified) + File.WriteAllText(manifestPath, manifestJson.ToString(indent).Replace("\" : ", "\": ")); + } + } +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Installer.cs b/Installer/Assets/YOUR_PACKAGE_TITLE/Installer.cs new file mode 100644 index 0000000..c9ab403 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Installer.cs @@ -0,0 +1,28 @@ +/* +┌────────────────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-Package-Template) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the MIT License. │ +│ See the LICENSE file in the project root for more information. │ +└────────────────────────────────────────────────────────────────────────────┘ +*/ +#nullable enable +using UnityEditor; + +namespace YOUR_PACKAGE_ID.Installer +{ + [InitializeOnLoad] + public static partial class Installer + { + public const string PackageId = "YOUR_PACKAGE_ID_LOWERCASE"; + public const string Version = "1.0.0"; + + static Installer() + { +#if !IVAN_MURZAK_INSTALLER_PROJECT + AddScopedRegistryIfNeeded(ManifestPath); +#endif + } + } +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/PackageExporter.cs b/Installer/Assets/YOUR_PACKAGE_TITLE/PackageExporter.cs new file mode 100644 index 0000000..3bcfbb7 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/PackageExporter.cs @@ -0,0 +1,37 @@ +/* +┌────────────────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-Package-Template) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the MIT License. │ +│ See the LICENSE file in the project root for more information. │ +└────────────────────────────────────────────────────────────────────────────┘ +*/ +#nullable enable +using UnityEngine; +using UnityEditor; +using System.IO; + +namespace YOUR_PACKAGE_ID.Installer +{ + public static class PackageExporter + { + public static void ExportPackage() + { + var packagePath = "Assets/YOUR_PACKAGE_NAME_INSTALLER"; + var outputPath = "build/YOUR_PACKAGE_NAME_INSTALLER_FILE.unitypackage"; + + // Ensure build directory exists + var buildDir = Path.GetDirectoryName(outputPath); + if (!Directory.Exists(buildDir)) + { + Directory.CreateDirectory(buildDir); + } + + // Export the package + AssetDatabase.ExportPackage(packagePath, outputPath, ExportPackageOptions.Recurse); + + Debug.Log($"Package exported to: {outputPath}"); + } + } +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/README.md b/Installer/Assets/YOUR_PACKAGE_TITLE/README.md new file mode 100644 index 0000000..f652c81 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/README.md @@ -0,0 +1,150 @@ +# Unity Package Template + +Stats + +Unity Editor supports NPM packages. It is way more flexible solution in comparison with classic Plugin that Unity is using for years. NPM package supports versioning and dependencies. You may update / downgrade any package very easily. Also, Unity Editor has UPM (Unity Package Manager) that makes the process even simpler. + +This template repository is designed to be easily updated into a real Unity package. Please follow the instruction bellow, it will help you to go through the entire process of package creation, distribution and installing. + +# Steps to make your package + +#### 1️⃣ Click the button to create new repository on GitHub using this template. + +[![create new repository](https://user-images.githubusercontent.com/9135028/198753285-3d3c9601-0711-43c7-a8f2-d40ec42393a2.png)](https://github.com/IvanMurzak/Unity-Package-Template/generate) + +#### 2️⃣ Clone your new repository and open it in Unity Editor + +#### 3️⃣ Rename `Package` + +Your package should have unique identifier. It is called a `name` of the package. It support only limited symbols. There is a sample of the package name. + +```text +com.github.your_name.package +``` + +- 👉 Instead of the word `package` use a word or couple of words that explains the main purpose of the package. +- 👉 The `name` should be unique in the world. + +###### Option 1: Use script to rename package (recommended) + +For MacOS + +```bash + +``` + +For Windows + +```bash +cd Commands +.\package_rename.bat Username PackageName +``` + +###### Option 2: Manual package rename + +Follow the instruction - [manual package rename](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Manual-Package-Rename.md) + + +#### 3️⃣ Customize `Assets/root/package.json` + +- 👉 **Update** `name` + > Sample: `com.github.your_name.package` + > Instead of the word `package` use a word or couple of words that explains the main purpose of the package. + > The `name` should be unique in the world. + +- 👉 **Update** `unity` to setup minimum supported Unity version +- 👉 **Update** + - `displayName` - visible name of the package, + - `version` - the version of the package (1.0.0), + - `description` - short description of the package, + - `author` - author of the package and url to the author (could be GitHub profile), + - `keywords` - array of keywords that describes the package. + +#### 4️⃣ Do you need Tests? + +
+ ❌ NO + +- 👉 **Delete** `Assets/root/Tests` folder +- 👉 **Delete** `.github/workflows` folder + +
+ +
+ ✅ YES + +- 👉 Make sure you executed `package-rename` script from the step #2. If not, please follow [manual package rename](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Manual-Package-Rename.md) instructions + +- 👉 Add GitHub Secrets + > At the GitHub repository, go to "Settings", then "Secrets and Variables", then "Actions", then click on "New repository secret" + 1. Add `UNITY_EMAIL` - email of your Unity ID's account + 2. Add `UNITY_PASSWORD` - password of your Unity ID's account + 3. Add `UNITY_LICENSE` - license content. Could be taken from `Unity_lic.ulf` file. Just open it in any text editor and copy the entire content + 1. Windows: The `Unity_lic.ulf` file is located at `C:/ProgramData/Unity/Unity_lic.ulf` + 2. MacOS: `/Library/Application Support/Unity/Unity_lic.ulf` + 3. Linux: `~/.local/share/unity3d/Unity/Unity_lic.ulf` + +
+ +#### 4️⃣ Add files into `Assets/root` folder + +[Unity guidelines](https://docs.unity3d.com/Manual/cus-layout.html) about organizing files into the package root directory + +```text + + ├── package.json + ├── README.md + ├── CHANGELOG.md + ├── LICENSE.md + ├── Third Party Notices.md + ├── Editor + │ ├── [company-name].[package-name].Editor.asmdef + │ └── EditorExample.cs + ├── Runtime + │ ├── [company-name].[package-name].asmdef + │ └── RuntimeExample.cs + ├── Tests + │ ├── Editor + │ │ ├── [company-name].[package-name].Editor.Tests.asmdef + │ │ └── EditorExampleTest.cs + │ └── Runtime + │ ├── [company-name].[package-name].Tests.asmdef + │ └── RuntimeExampleTest.cs + ├── Samples~ + │ ├── SampleFolder1 + │ ├── SampleFolder2 + │ └── ... + └── Documentation~ + └── [package-name].md +``` + +##### Final polishing + +- Update the `README.md` file (this file) with information about your package. +- Copy the updated `README.md` to `Assets/root` as well. + +> ⚠️ Everything outside of the `root` folder won't be added to your package. But still could be used for testing or showcasing your package at your repository. + +#### 5️⃣ Deploy to any registry you like + +- [Deploy to OpenUPM](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-OpenUPM.md) (recommended) +- [Deploy using GitHub](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-GitHub.md) +- [Deploy to npmjs.com](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-npmjs.md) + +#### 6️⃣ Install your package into Unity Project + +When your package is distributed, you can install it into any Unity project. + +> Don't install into the same Unity project, please use another one. + +- [Install OpenUPM-CLI](https://github.com/openupm/openupm-cli#installation) +- Open a command line at the root of Unity project (the folder which contains `Assets`) +- Execute the command (for `OpenUPM` hosted package) + + ```bash + openupm add YOUR_PACKAGE_NAME + ``` + +# Final view in Unity Package Manager + +![image](https://user-images.githubusercontent.com/9135028/198777922-fdb71949-aee7-49c8-800f-7db885de9453.png) diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/SimpleJSON.cs b/Installer/Assets/YOUR_PACKAGE_TITLE/SimpleJSON.cs new file mode 100644 index 0000000..93b37f9 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/SimpleJSON.cs @@ -0,0 +1,1434 @@ +/* * * * * + * A simple JSON Parser / builder + * ------------------------------ + * + * It mainly has been written as a simple JSON parser. It can build a JSON string + * from the node-tree, or generate a node tree from any valid JSON string. + * + * Written by Bunny83 + * 2012-06-09 + * + * Changelog now external. See Changelog.txt + * + * The MIT License (MIT) + * + * Copyright (c) 2012-2022 Markus Göbel (Bunny83) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * * * * */ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace YOUR_PACKAGE_ID.Installer.SimpleJSON +{ + public enum JSONNodeType + { + Array = 1, + Object = 2, + String = 3, + Number = 4, + NullValue = 5, + Boolean = 6, + None = 7, + Custom = 0xFF, + } + public enum JSONTextMode + { + Compact, + Indent + } + + public abstract partial class JSONNode + { + #region Enumerators + public struct Enumerator + { + private enum Type { None, Array, Object } + private Type type; + private Dictionary.Enumerator m_Object; + private List.Enumerator m_Array; + public bool IsValid { get { return type != Type.None; } } + public Enumerator(List.Enumerator aArrayEnum) + { + type = Type.Array; + m_Object = default(Dictionary.Enumerator); + m_Array = aArrayEnum; + } + public Enumerator(Dictionary.Enumerator aDictEnum) + { + type = Type.Object; + m_Object = aDictEnum; + m_Array = default(List.Enumerator); + } + public KeyValuePair Current + { + get + { + if (type == Type.Array) + return new KeyValuePair(string.Empty, m_Array.Current); + else if (type == Type.Object) + return m_Object.Current; + return new KeyValuePair(string.Empty, null); + } + } + public bool MoveNext() + { + if (type == Type.Array) + return m_Array.MoveNext(); + else if (type == Type.Object) + return m_Object.MoveNext(); + return false; + } + } + public struct ValueEnumerator + { + private Enumerator m_Enumerator; + public ValueEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } + public ValueEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } + public ValueEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } + public JSONNode Current { get { return m_Enumerator.Current.Value; } } + public bool MoveNext() { return m_Enumerator.MoveNext(); } + public ValueEnumerator GetEnumerator() { return this; } + } + public struct KeyEnumerator + { + private Enumerator m_Enumerator; + public KeyEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } + public KeyEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } + public KeyEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } + public string Current { get { return m_Enumerator.Current.Key; } } + public bool MoveNext() { return m_Enumerator.MoveNext(); } + public KeyEnumerator GetEnumerator() { return this; } + } + + public class LinqEnumerator : IEnumerator>, IEnumerable> + { + private JSONNode m_Node; + private Enumerator m_Enumerator; + internal LinqEnumerator(JSONNode aNode) + { + m_Node = aNode; + if (m_Node != null) + m_Enumerator = m_Node.GetEnumerator(); + } + public KeyValuePair Current { get { return m_Enumerator.Current; } } + object IEnumerator.Current { get { return m_Enumerator.Current; } } + public bool MoveNext() { return m_Enumerator.MoveNext(); } + + public void Dispose() + { + m_Node = null; + m_Enumerator = new Enumerator(); + } + + public IEnumerator> GetEnumerator() + { + return new LinqEnumerator(m_Node); + } + + public void Reset() + { + if (m_Node != null) + m_Enumerator = m_Node.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new LinqEnumerator(m_Node); + } + } + + #endregion Enumerators + + #region common interface + + public static bool forceASCII = false; // Use Unicode by default + public static bool longAsString = false; // lazy creator creates a JSONString instead of JSONNumber + public static bool allowLineComments = true; // allow "//"-style comments at the end of a line + + public abstract JSONNodeType Tag { get; } + + public virtual JSONNode this[int aIndex] { get { return null; } set { } } + + public virtual JSONNode this[string aKey] { get { return null; } set { } } + + public virtual string Value { get { return ""; } set { } } + + public virtual int Count { get { return 0; } } + + public virtual bool IsNumber { get { return false; } } + public virtual bool IsString { get { return false; } } + public virtual bool IsBoolean { get { return false; } } + public virtual bool IsNull { get { return false; } } + public virtual bool IsArray { get { return false; } } + public virtual bool IsObject { get { return false; } } + + public virtual bool Inline { get { return false; } set { } } + + public virtual void Add(string aKey, JSONNode aItem) + { + } + public virtual void Add(JSONNode aItem) + { + Add("", aItem); + } + + public virtual JSONNode Remove(string aKey) + { + return null; + } + + public virtual JSONNode Remove(int aIndex) + { + return null; + } + + public virtual JSONNode Remove(JSONNode aNode) + { + return aNode; + } + public virtual void Clear() { } + + public virtual JSONNode Clone() + { + return null; + } + + public virtual IEnumerable Children + { + get + { + yield break; + } + } + + public IEnumerable DeepChildren + { + get + { + foreach (var C in Children) + foreach (var D in C.DeepChildren) + yield return D; + } + } + + public virtual bool HasKey(string aKey) + { + return false; + } + + public virtual JSONNode GetValueOrDefault(string aKey, JSONNode aDefault) + { + return aDefault; + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + WriteToStringBuilder(sb, 0, 0, JSONTextMode.Compact); + return sb.ToString(); + } + + public virtual string ToString(int aIndent) + { + StringBuilder sb = new StringBuilder(); + WriteToStringBuilder(sb, 0, aIndent, JSONTextMode.Indent); + return sb.ToString(); + } + internal abstract void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode); + + public abstract Enumerator GetEnumerator(); + public IEnumerable> Linq { get { return new LinqEnumerator(this); } } + public KeyEnumerator Keys { get { return new KeyEnumerator(GetEnumerator()); } } + public ValueEnumerator Values { get { return new ValueEnumerator(GetEnumerator()); } } + + #endregion common interface + + #region typecasting properties + + + public virtual double AsDouble + { + get + { + double v = 0.0; + if (double.TryParse(Value, NumberStyles.Float, CultureInfo.InvariantCulture, out v)) + return v; + return 0.0; + } + set + { + Value = value.ToString(CultureInfo.InvariantCulture); + } + } + + public virtual int AsInt + { + get { return (int)AsDouble; } + set { AsDouble = value; } + } + + public virtual float AsFloat + { + get { return (float)AsDouble; } + set { AsDouble = value; } + } + + public virtual bool AsBool + { + get + { + bool v = false; + if (bool.TryParse(Value, out v)) + return v; + return !string.IsNullOrEmpty(Value); + } + set + { + Value = (value) ? "true" : "false"; + } + } + + public virtual long AsLong + { + get + { + long val = 0; + if (long.TryParse(Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) + return val; + return 0L; + } + set + { + Value = value.ToString(CultureInfo.InvariantCulture); + } + } + + public virtual ulong AsULong + { + get + { + ulong val = 0; + if (ulong.TryParse(Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) + return val; + return 0; + } + set + { + Value = value.ToString(CultureInfo.InvariantCulture); + } + } + + public virtual JSONArray AsArray + { + get + { + return this as JSONArray; + } + } + + public virtual JSONObject AsObject + { + get + { + return this as JSONObject; + } + } + + + #endregion typecasting properties + + #region operators + + public static implicit operator JSONNode(string s) + { + return (s == null) ? (JSONNode)JSONNull.CreateOrGet() : new JSONString(s); + } + public static implicit operator string(JSONNode d) + { + return (d == null) ? null : d.Value; + } + + public static implicit operator JSONNode(double n) + { + return new JSONNumber(n); + } + public static implicit operator double(JSONNode d) + { + return (d == null) ? 0 : d.AsDouble; + } + + public static implicit operator JSONNode(float n) + { + return new JSONNumber(n); + } + public static implicit operator float(JSONNode d) + { + return (d == null) ? 0 : d.AsFloat; + } + + public static implicit operator JSONNode(int n) + { + return new JSONNumber(n); + } + public static implicit operator int(JSONNode d) + { + return (d == null) ? 0 : d.AsInt; + } + + public static implicit operator JSONNode(long n) + { + if (longAsString) + return new JSONString(n.ToString(CultureInfo.InvariantCulture)); + return new JSONNumber(n); + } + public static implicit operator long(JSONNode d) + { + return (d == null) ? 0L : d.AsLong; + } + + public static implicit operator JSONNode(ulong n) + { + if (longAsString) + return new JSONString(n.ToString(CultureInfo.InvariantCulture)); + return new JSONNumber(n); + } + public static implicit operator ulong(JSONNode d) + { + return (d == null) ? 0 : d.AsULong; + } + + public static implicit operator JSONNode(bool b) + { + return new JSONBool(b); + } + public static implicit operator bool(JSONNode d) + { + return (d == null) ? false : d.AsBool; + } + + public static implicit operator JSONNode(KeyValuePair aKeyValue) + { + return aKeyValue.Value; + } + + public static bool operator ==(JSONNode a, object b) + { + if (ReferenceEquals(a, b)) + return true; + bool aIsNull = a is JSONNull || ReferenceEquals(a, null) || a is JSONLazyCreator; + bool bIsNull = b is JSONNull || ReferenceEquals(b, null) || b is JSONLazyCreator; + if (aIsNull && bIsNull) + return true; + return !aIsNull && a.Equals(b); + } + + public static bool operator !=(JSONNode a, object b) + { + return !(a == b); + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + #endregion operators + + [ThreadStatic] + private static StringBuilder m_EscapeBuilder; + internal static StringBuilder EscapeBuilder + { + get + { + if (m_EscapeBuilder == null) + m_EscapeBuilder = new StringBuilder(); + return m_EscapeBuilder; + } + } + internal static string Escape(string aText) + { + var sb = EscapeBuilder; + sb.Length = 0; + if (sb.Capacity < aText.Length + aText.Length / 10) + sb.Capacity = aText.Length + aText.Length / 10; + foreach (char c in aText) + { + switch (c) + { + case '\\': + sb.Append("\\\\"); + break; + case '\"': + sb.Append("\\\""); + break; + case '\n': + sb.Append("\\n"); + break; + case '\r': + sb.Append("\\r"); + break; + case '\t': + sb.Append("\\t"); + break; + case '\b': + sb.Append("\\b"); + break; + case '\f': + sb.Append("\\f"); + break; + default: + if (c < ' ' || (forceASCII && c > 127)) + { + ushort val = c; + sb.Append("\\u").Append(val.ToString("X4")); + } + else + sb.Append(c); + break; + } + } + string result = sb.ToString(); + sb.Length = 0; + return result; + } + + private static JSONNode ParseElement(string token, bool quoted) + { + if (quoted) + return token; + if (token.Length <= 5) + { + string tmp = token.ToLower(); + if (tmp == "false" || tmp == "true") + return tmp == "true"; + if (tmp == "null") + return JSONNull.CreateOrGet(); + } + double val; + if (double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out val)) + return val; + else + return token; + } + + public static JSONNode Parse(string aJSON) + { + Stack stack = new Stack(); + JSONNode ctx = null; + int i = 0; + StringBuilder Token = new StringBuilder(); + string TokenName = ""; + bool QuoteMode = false; + bool TokenIsQuoted = false; + bool HasNewlineChar = false; + while (i < aJSON.Length) + { + switch (aJSON[i]) + { + case '{': + if (QuoteMode) + { + Token.Append(aJSON[i]); + break; + } + stack.Push(new JSONObject()); + if (ctx != null) + { + ctx.Add(TokenName, stack.Peek()); + } + TokenName = ""; + Token.Length = 0; + ctx = stack.Peek(); + HasNewlineChar = false; + break; + + case '[': + if (QuoteMode) + { + Token.Append(aJSON[i]); + break; + } + + stack.Push(new JSONArray()); + if (ctx != null) + { + ctx.Add(TokenName, stack.Peek()); + } + TokenName = ""; + Token.Length = 0; + ctx = stack.Peek(); + HasNewlineChar = false; + break; + + case '}': + case ']': + if (QuoteMode) + { + + Token.Append(aJSON[i]); + break; + } + if (stack.Count == 0) + throw new Exception("JSON Parse: Too many closing brackets"); + + stack.Pop(); + if (Token.Length > 0 || TokenIsQuoted) + ctx.Add(TokenName, ParseElement(Token.ToString(), TokenIsQuoted)); + if (ctx != null) + ctx.Inline = !HasNewlineChar; + TokenIsQuoted = false; + TokenName = ""; + Token.Length = 0; + if (stack.Count > 0) + ctx = stack.Peek(); + break; + + case ':': + if (QuoteMode) + { + Token.Append(aJSON[i]); + break; + } + TokenName = Token.ToString(); + Token.Length = 0; + TokenIsQuoted = false; + break; + + case '"': + QuoteMode ^= true; + TokenIsQuoted |= QuoteMode; + break; + + case ',': + if (QuoteMode) + { + Token.Append(aJSON[i]); + break; + } + if (Token.Length > 0 || TokenIsQuoted) + ctx.Add(TokenName, ParseElement(Token.ToString(), TokenIsQuoted)); + TokenIsQuoted = false; + TokenName = ""; + Token.Length = 0; + TokenIsQuoted = false; + break; + + case '\r': + case '\n': + HasNewlineChar = true; + break; + + case ' ': + case '\t': + if (QuoteMode) + Token.Append(aJSON[i]); + break; + + case '\\': + ++i; + if (QuoteMode) + { + char C = aJSON[i]; + switch (C) + { + case 't': + Token.Append('\t'); + break; + case 'r': + Token.Append('\r'); + break; + case 'n': + Token.Append('\n'); + break; + case 'b': + Token.Append('\b'); + break; + case 'f': + Token.Append('\f'); + break; + case 'u': + { + string s = aJSON.Substring(i + 1, 4); + Token.Append((char)int.Parse( + s, + System.Globalization.NumberStyles.AllowHexSpecifier)); + i += 4; + break; + } + default: + Token.Append(C); + break; + } + } + break; + case '/': + if (allowLineComments && !QuoteMode && i + 1 < aJSON.Length && aJSON[i + 1] == '/') + { + while (++i < aJSON.Length && aJSON[i] != '\n' && aJSON[i] != '\r') ; + break; + } + Token.Append(aJSON[i]); + break; + case '\uFEFF': // remove / ignore BOM (Byte Order Mark) + break; + + default: + Token.Append(aJSON[i]); + break; + } + ++i; + } + if (QuoteMode) + { + throw new Exception("JSON Parse: Quotation marks seems to be messed up."); + } + if (ctx == null) + return ParseElement(Token.ToString(), TokenIsQuoted); + return ctx; + } + + } + // End of JSONNode + + public partial class JSONArray : JSONNode + { + private List m_List = new List(); + private bool inline = false; + public override bool Inline + { + get { return inline; } + set { inline = value; } + } + + public override JSONNodeType Tag { get { return JSONNodeType.Array; } } + public override bool IsArray { get { return true; } } + public override Enumerator GetEnumerator() { return new Enumerator(m_List.GetEnumerator()); } + + public override JSONNode this[int aIndex] + { + get + { + if (aIndex < 0 || aIndex >= m_List.Count) + return new JSONLazyCreator(this); + return m_List[aIndex]; + } + set + { + if (value == null) + value = JSONNull.CreateOrGet(); + if (aIndex < 0 || aIndex >= m_List.Count) + m_List.Add(value); + else + m_List[aIndex] = value; + } + } + + public override JSONNode this[string aKey] + { + get { return new JSONLazyCreator(this); } + set + { + if (value == null) + value = JSONNull.CreateOrGet(); + m_List.Add(value); + } + } + + public override int Count + { + get { return m_List.Count; } + } + + public override void Add(string aKey, JSONNode aItem) + { + if (aItem == null) + aItem = JSONNull.CreateOrGet(); + m_List.Add(aItem); + } + + public override JSONNode Remove(int aIndex) + { + if (aIndex < 0 || aIndex >= m_List.Count) + return null; + JSONNode tmp = m_List[aIndex]; + m_List.RemoveAt(aIndex); + return tmp; + } + + public override JSONNode Remove(JSONNode aNode) + { + m_List.Remove(aNode); + return aNode; + } + + public override void Clear() + { + m_List.Clear(); + } + + public override JSONNode Clone() + { + var node = new JSONArray(); + node.m_List.Capacity = m_List.Capacity; + foreach (var n in m_List) + { + if (n != null) + node.Add(n.Clone()); + else + node.Add(null); + } + return node; + } + + public override IEnumerable Children + { + get + { + foreach (JSONNode N in m_List) + yield return N; + } + } + + + internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) + { + aSB.Append('['); + int count = m_List.Count; + if (inline) + aMode = JSONTextMode.Compact; + for (int i = 0; i < count; i++) + { + if (i > 0) + aSB.Append(','); + if (aMode == JSONTextMode.Indent) + aSB.AppendLine(); + + if (aMode == JSONTextMode.Indent) + aSB.Append(' ', aIndent + aIndentInc); + m_List[i].WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); + } + if (aMode == JSONTextMode.Indent) + aSB.AppendLine().Append(' ', aIndent); + aSB.Append(']'); + } + } + // End of JSONArray + + public partial class JSONObject : JSONNode + { + private Dictionary m_Dict = new Dictionary(); + + private bool inline = false; + public override bool Inline + { + get { return inline; } + set { inline = value; } + } + + public override JSONNodeType Tag { get { return JSONNodeType.Object; } } + public override bool IsObject { get { return true; } } + + public override Enumerator GetEnumerator() { return new Enumerator(m_Dict.GetEnumerator()); } + + + public override JSONNode this[string aKey] + { + get + { + if (m_Dict.TryGetValue(aKey, out JSONNode outJsonNode)) + return outJsonNode; + else + return new JSONLazyCreator(this, aKey); + } + set + { + if (value == null) + value = JSONNull.CreateOrGet(); + if (m_Dict.ContainsKey(aKey)) + m_Dict[aKey] = value; + else + m_Dict.Add(aKey, value); + } + } + + public override JSONNode this[int aIndex] + { + get + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return null; + return m_Dict.ElementAt(aIndex).Value; + } + set + { + if (value == null) + value = JSONNull.CreateOrGet(); + if (aIndex < 0 || aIndex >= m_Dict.Count) + return; + string key = m_Dict.ElementAt(aIndex).Key; + m_Dict[key] = value; + } + } + + public override int Count + { + get { return m_Dict.Count; } + } + + public override void Add(string aKey, JSONNode aItem) + { + if (aItem == null) + aItem = JSONNull.CreateOrGet(); + + if (aKey != null) + { + if (m_Dict.ContainsKey(aKey)) + m_Dict[aKey] = aItem; + else + m_Dict.Add(aKey, aItem); + } + else + m_Dict.Add(Guid.NewGuid().ToString(), aItem); + } + + public override JSONNode Remove(string aKey) + { + if (!m_Dict.ContainsKey(aKey)) + return null; + JSONNode tmp = m_Dict[aKey]; + m_Dict.Remove(aKey); + return tmp; + } + + public override JSONNode Remove(int aIndex) + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return null; + var item = m_Dict.ElementAt(aIndex); + m_Dict.Remove(item.Key); + return item.Value; + } + + public override JSONNode Remove(JSONNode aNode) + { + try + { + var item = m_Dict.Where(k => k.Value == aNode).First(); + m_Dict.Remove(item.Key); + return aNode; + } + catch + { + return null; + } + } + + public override void Clear() + { + m_Dict.Clear(); + } + + public override JSONNode Clone() + { + var node = new JSONObject(); + foreach (var n in m_Dict) + { + node.Add(n.Key, n.Value.Clone()); + } + return node; + } + + public override bool HasKey(string aKey) + { + return m_Dict.ContainsKey(aKey); + } + + public override JSONNode GetValueOrDefault(string aKey, JSONNode aDefault) + { + JSONNode res; + if (m_Dict.TryGetValue(aKey, out res)) + return res; + return aDefault; + } + + public override IEnumerable Children + { + get + { + foreach (KeyValuePair N in m_Dict) + yield return N.Value; + } + } + + internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) + { + aSB.Append('{'); + bool first = true; + if (inline) + aMode = JSONTextMode.Compact; + foreach (var k in m_Dict) + { + if (!first) + aSB.Append(','); + first = false; + if (aMode == JSONTextMode.Indent) + aSB.AppendLine(); + if (aMode == JSONTextMode.Indent) + aSB.Append(' ', aIndent + aIndentInc); + aSB.Append('\"').Append(Escape(k.Key)).Append('\"'); + if (aMode == JSONTextMode.Compact) + aSB.Append(':'); + else + aSB.Append(" : "); + k.Value.WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); + } + if (aMode == JSONTextMode.Indent) + aSB.AppendLine().Append(' ', aIndent); + aSB.Append('}'); + } + + } + // End of JSONObject + + public partial class JSONString : JSONNode + { + private string m_Data; + + public override JSONNodeType Tag { get { return JSONNodeType.String; } } + public override bool IsString { get { return true; } } + + public override Enumerator GetEnumerator() { return new Enumerator(); } + + + public override string Value + { + get { return m_Data; } + set + { + m_Data = value; + } + } + + public JSONString(string aData) + { + m_Data = aData; + } + public override JSONNode Clone() + { + return new JSONString(m_Data); + } + + internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) + { + aSB.Append('\"').Append(Escape(m_Data)).Append('\"'); + } + public override bool Equals(object obj) + { + if (base.Equals(obj)) + return true; + string s = obj as string; + if (s != null) + return m_Data == s; + JSONString s2 = obj as JSONString; + if (s2 != null) + return m_Data == s2.m_Data; + return false; + } + public override int GetHashCode() + { + return m_Data.GetHashCode(); + } + public override void Clear() + { + m_Data = ""; + } + } + // End of JSONString + + public partial class JSONNumber : JSONNode + { + private double m_Data; + + public override JSONNodeType Tag { get { return JSONNodeType.Number; } } + public override bool IsNumber { get { return true; } } + public override Enumerator GetEnumerator() { return new Enumerator(); } + + public override string Value + { + get { return m_Data.ToString(CultureInfo.InvariantCulture); } + set + { + double v; + if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out v)) + m_Data = v; + } + } + + public override double AsDouble + { + get { return m_Data; } + set { m_Data = value; } + } + public override long AsLong + { + get { return (long)m_Data; } + set { m_Data = value; } + } + public override ulong AsULong + { + get { return (ulong)m_Data; } + set { m_Data = value; } + } + + public JSONNumber(double aData) + { + m_Data = aData; + } + + public JSONNumber(string aData) + { + Value = aData; + } + + public override JSONNode Clone() + { + return new JSONNumber(m_Data); + } + + internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) + { + aSB.Append(Value.ToString(CultureInfo.InvariantCulture)); + } + private static bool IsNumeric(object value) + { + return value is int || value is uint + || value is float || value is double + || value is decimal + || value is long || value is ulong + || value is short || value is ushort + || value is sbyte || value is byte; + } + public override bool Equals(object obj) + { + if (obj == null) + return false; + if (base.Equals(obj)) + return true; + JSONNumber s2 = obj as JSONNumber; + if (s2 != null) + return m_Data == s2.m_Data; + if (IsNumeric(obj)) + return Convert.ToDouble(obj) == m_Data; + return false; + } + public override int GetHashCode() + { + return m_Data.GetHashCode(); + } + public override void Clear() + { + m_Data = 0; + } + } + // End of JSONNumber + + public partial class JSONBool : JSONNode + { + private bool m_Data; + + public override JSONNodeType Tag { get { return JSONNodeType.Boolean; } } + public override bool IsBoolean { get { return true; } } + public override Enumerator GetEnumerator() { return new Enumerator(); } + + public override string Value + { + get { return m_Data.ToString(); } + set + { + bool v; + if (bool.TryParse(value, out v)) + m_Data = v; + } + } + public override bool AsBool + { + get { return m_Data; } + set { m_Data = value; } + } + + public JSONBool(bool aData) + { + m_Data = aData; + } + + public JSONBool(string aData) + { + Value = aData; + } + + public override JSONNode Clone() + { + return new JSONBool(m_Data); + } + + internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) + { + aSB.Append((m_Data) ? "true" : "false"); + } + public override bool Equals(object obj) + { + if (obj == null) + return false; + if (obj is bool) + return m_Data == (bool)obj; + return false; + } + public override int GetHashCode() + { + return m_Data.GetHashCode(); + } + public override void Clear() + { + m_Data = false; + } + } + // End of JSONBool + + public partial class JSONNull : JSONNode + { + static JSONNull m_StaticInstance = new JSONNull(); + public static bool reuseSameInstance = true; + public static JSONNull CreateOrGet() + { + if (reuseSameInstance) + return m_StaticInstance; + return new JSONNull(); + } + private JSONNull() { } + + public override JSONNodeType Tag { get { return JSONNodeType.NullValue; } } + public override bool IsNull { get { return true; } } + public override Enumerator GetEnumerator() { return new Enumerator(); } + + public override string Value + { + get { return "null"; } + set { } + } + public override bool AsBool + { + get { return false; } + set { } + } + + public override JSONNode Clone() + { + return CreateOrGet(); + } + + public override bool Equals(object obj) + { + if (object.ReferenceEquals(this, obj)) + return true; + return (obj is JSONNull); + } + public override int GetHashCode() + { + return 0; + } + + internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) + { + aSB.Append("null"); + } + } + // End of JSONNull + + internal partial class JSONLazyCreator : JSONNode + { + private JSONNode m_Node = null; + private string m_Key = null; + public override JSONNodeType Tag { get { return JSONNodeType.None; } } + public override Enumerator GetEnumerator() { return new Enumerator(); } + + public JSONLazyCreator(JSONNode aNode) + { + m_Node = aNode; + m_Key = null; + } + + public JSONLazyCreator(JSONNode aNode, string aKey) + { + m_Node = aNode; + m_Key = aKey; + } + + private T Set(T aVal) where T : JSONNode + { + if (m_Key == null) + m_Node.Add(aVal); + else + m_Node.Add(m_Key, aVal); + m_Node = null; // Be GC friendly. + return aVal; + } + + public override JSONNode this[int aIndex] + { + get { return new JSONLazyCreator(this); } + set { Set(new JSONArray()).Add(value); } + } + + public override JSONNode this[string aKey] + { + get { return new JSONLazyCreator(this, aKey); } + set { Set(new JSONObject()).Add(aKey, value); } + } + + public override void Add(JSONNode aItem) + { + Set(new JSONArray()).Add(aItem); + } + + public override void Add(string aKey, JSONNode aItem) + { + Set(new JSONObject()).Add(aKey, aItem); + } + + public static bool operator ==(JSONLazyCreator a, object b) + { + if (b == null) + return true; + return System.Object.ReferenceEquals(a, b); + } + + public static bool operator !=(JSONLazyCreator a, object b) + { + return !(a == b); + } + + public override bool Equals(object obj) + { + if (obj == null) + return true; + return System.Object.ReferenceEquals(this, obj); + } + + public override int GetHashCode() + { + return 0; + } + + public override int AsInt + { + get { Set(new JSONNumber(0)); return 0; } + set { Set(new JSONNumber(value)); } + } + + public override float AsFloat + { + get { Set(new JSONNumber(0.0f)); return 0.0f; } + set { Set(new JSONNumber(value)); } + } + + public override double AsDouble + { + get { Set(new JSONNumber(0.0)); return 0.0; } + set { Set(new JSONNumber(value)); } + } + + public override long AsLong + { + get + { + if (longAsString) + Set(new JSONString("0")); + else + Set(new JSONNumber(0.0)); + return 0L; + } + set + { + if (longAsString) + Set(new JSONString(value.ToString(CultureInfo.InvariantCulture))); + else + Set(new JSONNumber(value)); + } + } + + public override ulong AsULong + { + get + { + if (longAsString) + Set(new JSONString("0")); + else + Set(new JSONNumber(0.0)); + return 0L; + } + set + { + if (longAsString) + Set(new JSONString(value.ToString(CultureInfo.InvariantCulture))); + else + Set(new JSONNumber(value)); + } + } + + public override bool AsBool + { + get { Set(new JSONBool(false)); return false; } + set { Set(new JSONBool(value)); } + } + + public override JSONArray AsArray + { + get { return Set(new JSONArray()); } + } + + public override JSONObject AsObject + { + get { return Set(new JSONObject()); } + } + internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) + { + aSB.Append("null"); + } + } + // End of JSONLazyCreator + + public static class JSON + { + public static JSONNode Parse(string aJSON) + { + return JSONNode.Parse(aJSON); + } + } +} diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/Correct/correct_manifest.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/Correct/correct_manifest.json new file mode 100644 index 0000000..2334066 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/Correct/correct_manifest.json @@ -0,0 +1,33 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + }, + "scopedRegistries": [ + { + "name": "package.openupm.com", + "url": "https://package.openupm.com", + "scopes": [ + "com.ivanmurzak", + "extensions.unity", + "org.nuget.com.ivanmurzak", + "org.nuget.microsoft", + "org.nuget.system", + "org.nuget.r3" + ] + } + ] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_empty_1.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_empty_1.json new file mode 100644 index 0000000..f94ad81 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_empty_1.json @@ -0,0 +1,20 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + }, + "scopedRegistries": [] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_empty_2.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_empty_2.json new file mode 100644 index 0000000..f94ad81 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_empty_2.json @@ -0,0 +1,20 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + }, + "scopedRegistries": [] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_gone.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_gone.json new file mode 100644 index 0000000..bcdb7a6 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_gone.json @@ -0,0 +1,19 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + } +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_empty.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_empty.json new file mode 100644 index 0000000..c556ab4 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_empty.json @@ -0,0 +1,26 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + }, + "scopedRegistries": [ + { + "name": "package.openupm.com", + "url": "https://package.openupm.com", + "scopes": [] + } + ] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_gone.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_gone.json new file mode 100644 index 0000000..5b0829d --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_gone.json @@ -0,0 +1,25 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + }, + "scopedRegistries": [ + { + "name": "package.openupm.com", + "url": "https://package.openupm.com" + } + ] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_1.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_1.json new file mode 100644 index 0000000..ecd2092 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_1.json @@ -0,0 +1,32 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + }, + "scopedRegistries": [ + { + "name": "package.openupm.com", + "url": "https://package.openupm.com", + "scopes": [ + "com.ivanmurzak", + "extensions.unity", + "org.nuget.com.ivanmurzak", + "org.nuget.microsoft", + "org.nuget.system" + ] + } + ] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_2.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_2.json new file mode 100644 index 0000000..dca084f --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_2.json @@ -0,0 +1,31 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + }, + "scopedRegistries": [ + { + "name": "package.openupm.com", + "url": "https://package.openupm.com", + "scopes": [ + "com.ivanmurzak", + "extensions.unity", + "org.nuget.com.ivanmurzak", + "org.nuget.microsoft" + ] + } + ] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_3.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_3.json new file mode 100644 index 0000000..5a2d1a0 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_3.json @@ -0,0 +1,30 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + }, + "scopedRegistries": [ + { + "name": "package.openupm.com", + "url": "https://package.openupm.com", + "scopes": [ + "com.ivanmurzak", + "extensions.unity", + "org.nuget.com.ivanmurzak" + ] + } + ] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_4.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_4.json new file mode 100644 index 0000000..1ce5ba4 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_4.json @@ -0,0 +1,29 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + }, + "scopedRegistries": [ + { + "name": "package.openupm.com", + "url": "https://package.openupm.com", + "scopes": [ + "com.ivanmurzak", + "extensions.unity" + ] + } + ] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_5.json b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_5.json new file mode 100644 index 0000000..18b1858 --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_5.json @@ -0,0 +1,28 @@ +{ + "dependencies": { + "com.unity.ide.visualstudio": "2.0.23", + "com.unity.test-framework": "1.1.33", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", + "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", + "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", + "org.nuget.microsoft.bcl.memory": "9.0.7", + "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", + "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.hosting": "9.0.7", + "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", + "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", + "org.nuget.r3": "1.3.0", + "org.nuget.system.text.json": "9.0.7", + "PACKAGE_ID": "PACKAGE_VERSION" + }, + "scopedRegistries": [ + { + "name": "package.openupm.com", + "url": "https://package.openupm.com", + "scopes": [ + "com.ivanmurzak" + ] + } + ] +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/ManifestInstallerTests.cs b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/ManifestInstallerTests.cs new file mode 100644 index 0000000..f5c266a --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/ManifestInstallerTests.cs @@ -0,0 +1,71 @@ +/* +┌────────────────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-Package-Template) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the MIT License. │ +│ See the LICENSE file in the project root for more information. │ +└────────────────────────────────────────────────────────────────────────────┘ +*/ +using System.IO; +using NUnit.Framework; +using UnityEngine; + +namespace YOUR_PACKAGE_ID.Installer.Tests +{ + public class ManifestInstallerTests + { + const string PackageIdTag = "PACKAGE_ID"; + const string PackageVersionTag = "PACKAGE_VERSION"; + const string FilesRoot = "Assets/YOUR_PACKAGE_NAME_INSTALLER/Tests/Files"; + const string FilesCopyRoot = "Temp/YOUR_PACKAGE_NAME_INSTALLER/Tests/Files"; + static string CorrectManifestPath => $"{FilesRoot}/Correct/correct_manifest.json"; + + [SetUp] + public void SetUp() + { + Debug.Log($"[{nameof(ManifestInstallerTests)}] SetUp"); + Directory.CreateDirectory(FilesCopyRoot); + } + + [TearDown] + public void TearDown() + { + Debug.Log($"[{nameof(ManifestInstallerTests)}] TearDown"); + + // var files = Directory.GetFiles(FilesCopyRoot, "*.json", SearchOption.TopDirectoryOnly); + // foreach (var file in files) + // File.Delete(file); + } + + [Test] + public void All() + { + var files = Directory.GetFiles(FilesRoot, "*.json", SearchOption.TopDirectoryOnly); + var correctManifest = File.ReadAllText(CorrectManifestPath) + .Replace(PackageVersionTag, Installer.Version) + .Replace(PackageIdTag, Installer.PackageId); + + foreach (var file in files) + { + Debug.Log($"Found JSON file: {file}"); + + // Copy the file + var fileCopy = Path.Combine(FilesCopyRoot, Path.GetFileName(file)); + File.Copy(file, fileCopy, overwrite: true); + + // Arrange + File.WriteAllText(fileCopy, File.ReadAllText(fileCopy) + .Replace(PackageVersionTag, Installer.Version) + .Replace(PackageIdTag, Installer.PackageId)); + + // Act + Installer.AddScopedRegistryIfNeeded(fileCopy); + + // Assert + var modifiedManifest = File.ReadAllText(fileCopy); + Assert.AreEqual(correctManifest, modifiedManifest, $"Modified manifest from {file} does not match the correct manifest."); + } + } + } +} diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/VersionComparisonTests.cs b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/VersionComparisonTests.cs new file mode 100644 index 0000000..21ed18c --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/VersionComparisonTests.cs @@ -0,0 +1,232 @@ +/* +┌────────────────────────────────────────────────────────────────────────────┐ +│ Author: Ivan Murzak (https://github.com/IvanMurzak) │ +│ Repository: GitHub (https://github.com/IvanMurzak/Unity-Package-Template) │ +│ Copyright (c) 2025 Ivan Murzak │ +│ Licensed under the MIT License. │ +│ See the LICENSE file in the project root for more information. │ +└────────────────────────────────────────────────────────────────────────────┘ +*/ +using System.IO; +using NUnit.Framework; +using YOUR_PACKAGE_ID.Installer.SimpleJSON; + +namespace YOUR_PACKAGE_ID.Installer.Tests +{ + public class VersionComparisonTests + { + const string TestManifestPath = "Temp/YOUR_PACKAGE_ID.Installer.Tests/test_manifest.json"; + const string PackageId = "YOUR_PACKAGE_ID_LOWERCASE"; + + [SetUp] + public void SetUp() + { + var dir = Path.GetDirectoryName(TestManifestPath); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + } + + [TearDown] + public void TearDown() + { + if (File.Exists(TestManifestPath)) + File.Delete(TestManifestPath); + } + + [Test] + public void ShouldUpdateVersion_PatchVersionHigher_ReturnsTrue() + { + // Act & Assert + Assert.IsTrue( + condition: Installer.ShouldUpdateVersion( + currentVersion: "1.5.1", + installerVersion: "1.5.2" + ), + message: "Should update when patch version is higher" + ); + } + + [Test] + public void ShouldUpdateVersion_PatchVersionLower_ReturnsFalse() + { + // Act & Assert + Assert.IsFalse( + condition: Installer.ShouldUpdateVersion( + currentVersion: "1.5.2", + installerVersion: "1.5.1" + ), + message: "Should not downgrade when patch version is lower" + ); + } + + [Test] + public void ShouldUpdateVersion_MinorVersionHigher_ReturnsTrue() + { + // Act & Assert + Assert.IsTrue( + condition: Installer.ShouldUpdateVersion( + currentVersion: "1.5.0", + installerVersion: "1.6.0" + ), + message: "Should update when minor version is higher" + ); + } + + [Test] + public void ShouldUpdateVersion_MinorVersionLower_ReturnsFalse() + { + // Act & Assert + Assert.IsFalse( + condition: Installer.ShouldUpdateVersion( + currentVersion: "1.6.0", + installerVersion: "1.5.0" + ), + message: "Should not downgrade when minor version is lower" + ); + } + + [Test] + public void ShouldUpdateVersion_MajorVersionHigher_ReturnsTrue() + { + // Act & Assert + Assert.IsTrue( + condition: Installer.ShouldUpdateVersion( + currentVersion: "1.5.0", + installerVersion: "2.0.0" + ), + message: "Should update when major version is higher" + ); + } + + [Test] + public void ShouldUpdateVersion_MajorVersionLower_ReturnsFalse() + { + // Act & Assert + Assert.IsFalse( + condition: Installer.ShouldUpdateVersion( + currentVersion: "2.0.0", + installerVersion: "1.5.0" + ), + message: "Should not downgrade when major version is lower" + ); + } + + [Test] + public void ShouldUpdateVersion_SameVersion_ReturnsFalse() + { + // Act & Assert + Assert.IsFalse( + condition: Installer.ShouldUpdateVersion( + currentVersion: "1.5.2", + installerVersion: "1.5.2" + ), + message: "Should not update when versions are the same" + ); + } + + [Test] + public void ShouldUpdateVersion_EmptyCurrentVersion_ReturnsTrue() + { + // Act & Assert + Assert.IsTrue( + condition: Installer.ShouldUpdateVersion( + currentVersion: "", + installerVersion: "1.5.2" + ), + message: "Should install when no current version exists" + ); + } + + [Test] + public void ShouldUpdateVersion_NullCurrentVersion_ReturnsTrue() + { + // Act & Assert + Assert.IsTrue( + condition: Installer.ShouldUpdateVersion( + currentVersion: null, + installerVersion: "1.5.2" + ), + message: "Should install when current version is null" + ); + } + + [Test] + public void AddScopedRegistryIfNeeded_PreventVersionDowngrade_Integration() + { + // Arrange - Create manifest with higher version + var versionParts = Installer.Version.Split('.'); + var majorVersion = int.Parse(versionParts[0]); + var higherVersion = $"{majorVersion + 10}.0.0"; + var manifest = new JSONObject + { + [Installer.Dependencies] = new JSONObject + { + [PackageId] = higherVersion + }, + [Installer.ScopedRegistries] = new JSONArray() + }; + File.WriteAllText(TestManifestPath, manifest.ToString(2)); + + // Act - Run installer (should NOT downgrade) + Installer.AddScopedRegistryIfNeeded(TestManifestPath); + + // Assert - Version should remain unchanged + var updatedContent = File.ReadAllText(TestManifestPath); + var updatedManifest = JSONObject.Parse(updatedContent); + var actualVersion = updatedManifest[Installer.Dependencies][PackageId]; + + Assert.AreEqual(higherVersion, actualVersion.ToString().Trim('"'), + "Version should not be downgraded from higher version"); + } + + [Test] + public void AddScopedRegistryIfNeeded_AllowVersionUpgrade_Integration() + { + // Arrange - Create manifest with lower version (0.0.1 is always lower) + var lowerVersion = "0.0.1"; + var manifest = new JSONObject + { + [Installer.Dependencies] = new JSONObject + { + [PackageId] = lowerVersion + }, + [Installer.ScopedRegistries] = new JSONArray() + }; + File.WriteAllText(TestManifestPath, manifest.ToString(2)); + + // Act - Run installer (should upgrade) + Installer.AddScopedRegistryIfNeeded(TestManifestPath); + + // Assert - Version should be upgraded to installer version + var updatedContent = File.ReadAllText(TestManifestPath); + var updatedManifest = JSONObject.Parse(updatedContent); + var actualVersion = updatedManifest[Installer.Dependencies][PackageId]; + + Assert.AreEqual(Installer.Version, actualVersion.ToString().Trim('"'), + "Version should be upgraded to installer version"); + } + + [Test] + public void AddScopedRegistryIfNeeded_NoExistingDependency_InstallsNewVersion() + { + // Arrange - Create manifest without the package + var manifest = new JSONObject + { + [Installer.Dependencies] = new JSONObject(), + [Installer.ScopedRegistries] = new JSONArray() + }; + File.WriteAllText(TestManifestPath, manifest.ToString(2)); + + // Act - Run installer + Installer.AddScopedRegistryIfNeeded(TestManifestPath); + + // Assert - Package should be added with installer version + var updatedContent = File.ReadAllText(TestManifestPath); + var updatedManifest = JSONObject.Parse(updatedContent); + var actualVersion = updatedManifest[Installer.Dependencies][PackageId]; + + Assert.AreEqual(Installer.Version, actualVersion.ToString().Trim('"'), + "New package should be installed with installer version"); + } + } +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/YOUR_PACKAGE_ID.Installer.Tests.asmdef b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/YOUR_PACKAGE_ID.Installer.Tests.asmdef new file mode 100644 index 0000000..2f3041b --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/Tests/YOUR_PACKAGE_ID.Installer.Tests.asmdef @@ -0,0 +1,22 @@ +{ + "name": "YOUR_PACKAGE_ID.Installer.Tests", + "rootNamespace": "YOUR_PACKAGE_ID.Installer.Tests", + "references": [ + "YOUR_PACKAGE_ID.Installer" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Installer/Assets/YOUR_PACKAGE_TITLE/YOUR_PACKAGE_ID.Installer.asmdef b/Installer/Assets/YOUR_PACKAGE_TITLE/YOUR_PACKAGE_ID.Installer.asmdef new file mode 100644 index 0000000..7b961aa --- /dev/null +++ b/Installer/Assets/YOUR_PACKAGE_TITLE/YOUR_PACKAGE_ID.Installer.asmdef @@ -0,0 +1,16 @@ +{ + "name": "YOUR_PACKAGE_ID.Installer", + "rootNamespace": "YOUR_PACKAGE_ID.Installer", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/README.md b/README.md index f652c81..cf9d725 100644 --- a/README.md +++ b/README.md @@ -14,79 +14,67 @@ This template repository is designed to be easily updated into a real Unity pack #### 2️⃣ Clone your new repository and open it in Unity Editor -#### 3️⃣ Rename `Package` +#### 3️⃣ Initialize Project -Your package should have unique identifier. It is called a `name` of the package. It support only limited symbols. There is a sample of the package name. +Use the initialization script to rename the package and replace all placeholders. -```text -com.github.your_name.package +```powershell +.\commands\init.ps1 -PackageId "com.company.package" -PackageName "My Package" ``` -- 👉 Instead of the word `package` use a word or couple of words that explains the main purpose of the package. -- 👉 The `name` should be unique in the world. +This script will: +- Rename directories and files. +- Replace `YOUR_PACKAGE_ID`, `YOUR_PACKAGE_NAME`, etc. in all files. -###### Option 1: Use script to rename package (recommended) +#### 4️⃣ Manual Configuration -For MacOS +1. **Update `package.json`** + Open `Unity-Package/Assets/root/package.json` and update: + - `description` + - `author` + - `keywords` + - `unity` (minimum supported Unity version) -```bash +2. **Generate Meta Files** + To ensure all Unity meta files are correctly generated: + - Open Unity Hub. + - Add the `Installer` folder as a project. + - Add the `Unity-Package` folder as a project. + - Open both projects in Unity Editor. This will generate the necessary `.meta` files. -``` +#### 5️⃣ Version Management -For Windows +To update the package version across all files (package.json, Installer.cs, etc.), use the bump version script: -```bash -cd Commands -.\package_rename.bat Username PackageName +```powershell +.\commands\bump-version.ps1 -NewVersion "1.0.1" ``` -###### Option 2: Manual package rename - -Follow the instruction - [manual package rename](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Manual-Package-Rename.md) - - -#### 3️⃣ Customize `Assets/root/package.json` - -- 👉 **Update** `name` - > Sample: `com.github.your_name.package` - > Instead of the word `package` use a word or couple of words that explains the main purpose of the package. - > The `name` should be unique in the world. - -- 👉 **Update** `unity` to setup minimum supported Unity version -- 👉 **Update** - - `displayName` - visible name of the package, - - `version` - the version of the package (1.0.0), - - `description` - short description of the package, - - `author` - author of the package and url to the author (could be GitHub profile), - - `keywords` - array of keywords that describes the package. - -#### 4️⃣ Do you need Tests? - -
- ❌ NO - -- 👉 **Delete** `Assets/root/Tests` folder -- 👉 **Delete** `.github/workflows` folder +#### 6️⃣ Setup CI/CD -
+To enable automatic testing and deployment: -
- ✅ YES +1. **Configure GitHub Secrets** + Go to `Settings` > `Secrets and variables` > `Actions` > `New repository secret` and add: + - `UNITY_EMAIL`: Your Unity account email. + - `UNITY_PASSWORD`: Your Unity account password. + - `UNITY_LICENSE`: Content of your `Unity_lic.ulf` file. + - Windows: `C:/ProgramData/Unity/Unity_lic.ulf` + - Mac: `/Library/Application Support/Unity/Unity_lic.ulf` + - Linux: `~/.local/share/unity3d/Unity/Unity_lic.ulf` -- 👉 Make sure you executed `package-rename` script from the step #2. If not, please follow [manual package rename](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Manual-Package-Rename.md) instructions +2. **Enable Workflows** + Rename the sample workflow files to enable them: + - `.github/workflows/release.yml-sample` ➡️ `.github/workflows/release.yml` + - `.github/workflows/test_pull_request.yml-sample` ➡️ `.github/workflows/test_pull_request.yml` -- 👉 Add GitHub Secrets - > At the GitHub repository, go to "Settings", then "Secrets and Variables", then "Actions", then click on "New repository secret" - 1. Add `UNITY_EMAIL` - email of your Unity ID's account - 2. Add `UNITY_PASSWORD` - password of your Unity ID's account - 3. Add `UNITY_LICENSE` - license content. Could be taken from `Unity_lic.ulf` file. Just open it in any text editor and copy the entire content - 1. Windows: The `Unity_lic.ulf` file is located at `C:/ProgramData/Unity/Unity_lic.ulf` - 2. MacOS: `/Library/Application Support/Unity/Unity_lic.ulf` - 3. Linux: `~/.local/share/unity3d/Unity/Unity_lic.ulf` +3. **Update Unity Version** + Open both `.yml` files and update the `UNITY_VERSION` (or similar variable) to match your project's Unity Editor version. -
+4. **Automatic Deployment** + The release workflow triggers automatically when you push to the `main` branch with an incremented version in `package.json`. -#### 4️⃣ Add files into `Assets/root` folder +#### 7️⃣ Add files into `Assets/root` folder [Unity guidelines](https://docs.unity3d.com/Manual/cus-layout.html) about organizing files into the package root directory @@ -125,13 +113,13 @@ Follow the instruction - [manual package rename](https://github.com/IvanMurzak/U > ⚠️ Everything outside of the `root` folder won't be added to your package. But still could be used for testing or showcasing your package at your repository. -#### 5️⃣ Deploy to any registry you like +#### 8️⃣ Deploy to any registry you like - [Deploy to OpenUPM](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-OpenUPM.md) (recommended) - [Deploy using GitHub](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-GitHub.md) - [Deploy to npmjs.com](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-npmjs.md) -#### 6️⃣ Install your package into Unity Project +#### 9️⃣ Install your package into Unity Project When your package is distributed, you can install it into any Unity project. diff --git a/Unity-Package/.gitignore b/Unity-Package/.gitignore new file mode 100644 index 0000000..1b0a9ff --- /dev/null +++ b/Unity-Package/.gitignore @@ -0,0 +1,78 @@ +# This .gitignore file should be placed at the root of your Unity project directory +# +# Get latest from https://github.com/github/gitignore/blob/main/Docs/Unity.gitignore +# +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Ll]ogs/ +/[Mm]emoryCaptures/ + +# Asset meta data should only be ignored when the corresponding asset is also ignored +!/[Aa]ssets/**/*.meta + +# Uncomment this line if you wish to ignore the asset store tools plugin +# /[Aa]ssets/AssetStoreTools* + +# Autogenerated Jetbrains Rider plugin +[Aa]ssets/Plugins/Editor/JetBrains* + +# Visual Studio cache directory +.vs/ + +# Gradle cache directory +.gradle/ + +# Autogenerated VS/MD/Consulo solution and project files +ExportedObj/ +.consulo/ +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db + +# Unity3D generated meta files +*.pidb.meta +*.pdb.meta +*.mdb.meta + +# Unity3D generated file on crash reports +sysinfo.txt + +# Builds +*.apk +*.unitypackage + +# Crashlytics generated file +crashlytics-build.properties + +*/AndroidLogcatSettings.asset +/Assets/StreamingAssets + +# Crashlytics generated file +crashlytics-build.properties + +# ODIN Ignore the auto-generated AOT compatibility dll. +/Assets/Plugins/Sirenix/Assemblies/AOT/* +/Assets/Plugins/Sirenix/Assemblies/AOT** + +# ODIN Ignore all unpacked demos. +/Assets/Plugins/Sirenix/Demos/* + +# ODIN plugin +/Assets/Plugins/Sirenix** + +# Claude +.claude \ No newline at end of file diff --git a/Assets/root/CHANGELOG.md b/Unity-Package/Assets/root/CHANGELOG.md similarity index 100% rename from Assets/root/CHANGELOG.md rename to Unity-Package/Assets/root/CHANGELOG.md diff --git a/Assets/root/Documentation~/.gitignore b/Unity-Package/Assets/root/Documentation~/.gitignore similarity index 100% rename from Assets/root/Documentation~/.gitignore rename to Unity-Package/Assets/root/Documentation~/.gitignore diff --git a/Assets/root/Editor/Gizmos/.gitignore b/Unity-Package/Assets/root/Editor/Gizmos/.gitignore similarity index 100% rename from Assets/root/Editor/Gizmos/.gitignore rename to Unity-Package/Assets/root/Editor/Gizmos/.gitignore diff --git a/Assets/root/Editor/Gizmos/icon.png b/Unity-Package/Assets/root/Editor/Gizmos/icon.png similarity index 100% rename from Assets/root/Editor/Gizmos/icon.png rename to Unity-Package/Assets/root/Editor/Gizmos/icon.png diff --git a/Assets/root/Editor/Scripts/.gitignore b/Unity-Package/Assets/root/Editor/Scripts/.gitignore similarity index 100% rename from Assets/root/Editor/Scripts/.gitignore rename to Unity-Package/Assets/root/Editor/Scripts/.gitignore diff --git a/Assets/root/Editor/Scripts/Package.Editor.asmdef b/Unity-Package/Assets/root/Editor/Scripts/YOUR_PACKAGE_ID.Editor.asmdef similarity index 80% rename from Assets/root/Editor/Scripts/Package.Editor.asmdef rename to Unity-Package/Assets/root/Editor/Scripts/YOUR_PACKAGE_ID.Editor.asmdef index 6d7adea..691b340 100644 --- a/Assets/root/Editor/Scripts/Package.Editor.asmdef +++ b/Unity-Package/Assets/root/Editor/Scripts/YOUR_PACKAGE_ID.Editor.asmdef @@ -1,7 +1,7 @@ { - "name": "Package.Editor", + "name": "YOUR_PACKAGE_ID.Editor", "references": [ - "Package.Runtime" + "YOUR_PACKAGE_ID.Runtime" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Assets/root/LICENSE b/Unity-Package/Assets/root/LICENSE similarity index 100% rename from Assets/root/LICENSE rename to Unity-Package/Assets/root/LICENSE diff --git a/Assets/root/Runtime/.gitignore b/Unity-Package/Assets/root/Runtime/.gitignore similarity index 100% rename from Assets/root/Runtime/.gitignore rename to Unity-Package/Assets/root/Runtime/.gitignore diff --git a/Assets/root/Runtime/Package.Runtime.asmdef b/Unity-Package/Assets/root/Runtime/YOUR_PACKAGE_ID.Runtime.asmdef similarity index 88% rename from Assets/root/Runtime/Package.Runtime.asmdef rename to Unity-Package/Assets/root/Runtime/YOUR_PACKAGE_ID.Runtime.asmdef index b0f1811..d4dd9e8 100644 --- a/Assets/root/Runtime/Package.Runtime.asmdef +++ b/Unity-Package/Assets/root/Runtime/YOUR_PACKAGE_ID.Runtime.asmdef @@ -1,5 +1,5 @@ { - "name": "Package.Runtime", + "name": "YOUR_PACKAGE_ID.Runtime", "references": [], "includePlatforms": [], "excludePlatforms": [ diff --git a/Assets/root/Samples~/.gitignore b/Unity-Package/Assets/root/Samples~/.gitignore similarity index 100% rename from Assets/root/Samples~/.gitignore rename to Unity-Package/Assets/root/Samples~/.gitignore diff --git a/Assets/root/Tests/Editor/.gitignore b/Unity-Package/Assets/root/Tests/Editor/.gitignore similarity index 100% rename from Assets/root/Tests/Editor/.gitignore rename to Unity-Package/Assets/root/Tests/Editor/.gitignore diff --git a/Assets/root/Tests/Editor/Package.Editor.Tests.asmdef b/Unity-Package/Assets/root/Tests/Editor/YOUR_PACKAGE_ID.Editor.Tests.asmdef similarity index 80% rename from Assets/root/Tests/Editor/Package.Editor.Tests.asmdef rename to Unity-Package/Assets/root/Tests/Editor/YOUR_PACKAGE_ID.Editor.Tests.asmdef index 7955ca2..656c48a 100644 --- a/Assets/root/Tests/Editor/Package.Editor.Tests.asmdef +++ b/Unity-Package/Assets/root/Tests/Editor/YOUR_PACKAGE_ID.Editor.Tests.asmdef @@ -1,10 +1,10 @@ { - "name": "Package.Editor.Tests", + "name": "YOUR_PACKAGE_ID.Editor.Tests", "references": [ "UnityEngine.TestRunner", "UnityEditor.TestRunner", - "Package.Runtime", - "Package.Editor" + "YOUR_PACKAGE_ID.Runtime", + "YOUR_PACKAGE_ID.Editor" ], "includePlatforms": [ "Editor" diff --git a/Assets/root/Tests/Runtime/.gitignore b/Unity-Package/Assets/root/Tests/Runtime/.gitignore similarity index 100% rename from Assets/root/Tests/Runtime/.gitignore rename to Unity-Package/Assets/root/Tests/Runtime/.gitignore diff --git a/Assets/root/Tests/Runtime/Package.Tests.asmdef b/Unity-Package/Assets/root/Tests/Runtime/YOUR_PACKAGE_ID.Tests.asmdef similarity index 84% rename from Assets/root/Tests/Runtime/Package.Tests.asmdef rename to Unity-Package/Assets/root/Tests/Runtime/YOUR_PACKAGE_ID.Tests.asmdef index 050940d..598335f 100644 --- a/Assets/root/Tests/Runtime/Package.Tests.asmdef +++ b/Unity-Package/Assets/root/Tests/Runtime/YOUR_PACKAGE_ID.Tests.asmdef @@ -1,8 +1,8 @@ { - "name": "Package.Tests", + "name": "YOUR_PACKAGE_ID.Tests", "references": [ "UnityEngine.TestRunner", - "Package.Runtime" + "YOUR_PACKAGE_ID.Runtime" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Assets/root/package.json b/Unity-Package/Assets/root/package.json similarity index 77% rename from Assets/root/package.json rename to Unity-Package/Assets/root/package.json index 79c91a4..dea3a50 100644 --- a/Assets/root/package.json +++ b/Unity-Package/Assets/root/package.json @@ -1,6 +1,6 @@ { - "name": "com.github.your_name.package", - "displayName": "Package", + "name": "YOUR_PACKAGE_ID_LOWERCASE", + "displayName": "YOUR_PACKAGE_NAME", "author": { "name": "Your_Name", "url": "https://github.com/Your_Name"