diff --git a/ReadMe.md b/ReadMe.md index 5ec254a..8d58a73 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -2,8 +2,15 @@ ## Summary -These scripts are to help me set up Git / Subversion repositories for UE4 without -having to remember everything. +These scripts are to help me with various repetetive or easy-to-forget tasks +related to [Unreal Engine 4](https://www.unrealengine.com). + +* [Setting up a project for Git / Git-LFS](#git-setup) +* [Packaging Builds](#packaging-builds) +* [Releasing builds to Itch, Steam](#releasing-builds) + + +## Git Setup Initially we'd decided to go back to Subversion because of the importance of good locking workflow for uasset/umap files in UE4, to prevent binary merge errors. @@ -26,7 +33,7 @@ We use a particular content workflow: Together the scripts below configure everything so I don't have to remember. -## Steps to set up Git + LFS for a UE4 project +### Steps to set up Git + LFS for a UE4 project 1. The script works for projects with no git repo yet, or those with an existing git repo 1. For existing repos, ideally you will not have committed any large files to Git yet @@ -39,7 +46,16 @@ Together the scripts below configure everything so I don't have to remember. 1. Push ALL BRANCHES of this new repo to the host of your choice -# Subversion Information (ignore below if using Git) +## Packaging builds + +.. TODO + +## Releasing Builds + +.. TODO + + +# LEGACY: Subversion Information (ignore below if using Git) ## Steps to create a new SVN repo for a UE4 project diff --git a/inc/packageconfig.ps1 b/inc/packageconfig.ps1 new file mode 100644 index 0000000..8dad318 --- /dev/null +++ b/inc/packageconfig.ps1 @@ -0,0 +1,117 @@ + +class PackageVariant { + # Name of the variant (can be anything) + [string]$Name + # Platform name (must be one supported by Unreal e.g. Win64) + [string]$Platform + # Configuration name i.e. Development, Shipping + [string]$Configuration + # Additional arguments to send to the build command line + [string]$ExtraBuildArguments + # Whether to create a zip of this package (default false) + [bool]$Zip + # The Steam application ID, if you intend to send this variant to Steam + [string]$SteamAppId + # The Steam depot ID, if you intend to send this variant to Steam + [string]$SteamDepotId + # Steam login to use to deploy to Steam (if you haven't cached your credential already you'll get a login prompt) + [string]$SteamLogin + # Itch application identifier e.g. your-account/game-name, if you intend to send this variant to Itch + [string]$ItchAppId + # Itch channel, if you intend to send this variant to Itch (usually a platform) + [string]$ItchChannel + + PackageVariant() { + $this.Configuration = "Development" + $this.Zip = $false + } + PackageVariant([PSCustomObject]$obj) { + $this.Configuration = "Development" + $this.Zip = $false + + # Override just properties that are set + $obj.PSObject.Properties | ForEach-Object { + try { + $this.$($_.Name) = $_.Value + } catch { + Write-Host "Invalid property for package variant: $($_.Name) = $($_.Value)" + } + } + + } +} + +# Our config for both building and releasing +# Note that environment variables also have an effect: +# - UE4INSTALL: a specific UE install to use (default blank, find a version in UE4ROOT) +# - UE4ROOT: Parent folder of all binary UE4 installs (default C:\Program Files\Epic Games) +class PackageConfig { + # The root of the folder structure which will contain packaged output + # Will be structured $OutputDir/$version/$variant + # If relative, will be considered relative to source folder + [string]$OutputDir + # Folder to place zipped releases (named $target_$platform_$variant_$version.zip) + # If relative, will be considered relative to source folder + [string]$ZipDir + # Target name: this will usually be the name of your game + [string]$Target + # Whether to cook all maps (default true) + [bool]$CookAllMaps + # If CookAllMaps=false, list the map names you want to cook + [array]$MapsIncluded + # If CookAllMaps=true, list the map names you want to exclude from cooking + [array]$MapsExcluded + # Whether to combine assets into a pak file (default true) + [bool]$UsePak + # Whether to compress the pak file (default false since deployments often compress & can detect diffs better) + [bool]$CompressPak + # List of PackageVariant entries + [array]$Variants + # Names of the default variant(s) to package / release if unspecified + [array]$DefaultVariants + + PackageConfig([PSCustomObject]$obj) { + # Construct from JSON object + $this.CookAllMaps = $true + $this.UsePak = $true + $this.CompressPak = $false + $this.Variants = @() + + # Override just properties that are set + $obj.PSObject.Properties | ForEach-Object { + if ($_.Name -ne "Variants") { + try { + # Nested array dealt with below + $this.$($_.Name) = $_.Value + } catch { + Write-Host "Invalid property in root package config: $($_.Name) = $($_.Value)" + } + } + } + + $this.Variants = $obj.Variants | ForEach-Object { + [PackageVariant]::New($_) + } + } + + +} + +# Read packageconfig.json file from a source location and return PackageConfig instance +function Read-Package-Config { + param ( + [string]$srcfolder + ) + + Write-Host "Hello!!!" + + $configfile = Resolve-Path "$srcfolder\packageconfig.json" + if (-not (Test-Path $configfile -PathType Leaf)) { + throw "$srcfolder\packageconfig.json does not exist!" + } + + $obj = (Get-Content $configfile) | ConvertFrom-Json + + return [PackageConfig]::New($obj) + +} diff --git a/inc/projectversion.ps1 b/inc/projectversion.ps1 new file mode 100644 index 0000000..00b6a9b --- /dev/null +++ b/inc/projectversion.ps1 @@ -0,0 +1,96 @@ +Import-Module PsIni + +function Get-Project-Version-Ini-Filename { + return Join-Path $srcfolder "Config/DefaultGame.ini" -Resolve +} + +function Get-Project-Version { + param ( + [string]$srcfolder + ) + + $file = Get-Project-Version-Ini-Filename + $gameIni = Get-IniContent $file + + return $gameIni["/Script/EngineSettings.GeneralProjectSettings"].ProjectVersion + +} +function Increment-Project-Version { + + param ( + [string]$srcfolder, + [bool]$major, + [bool]$minor, + [bool]$patch, + [bool]$hotfix, + [bool]$dryrun = $false + ) + + if (($major + $minor + $patch + $hotfix) -gt 1) { + throw "Can't set more than one of major/minor/patch/hotfix at the same time!" + } + + $gameIniFile = Get-Project-Version-Ini-Filename + $gameIni = Get-IniContent $gameIniFile + + Write-Verbose "[version++] M:$major m:$minor p:$patch h:$hotfix" + + # We have to use Write-Verbose now that we're using the return value, Write-Output + # appends to the return value. Write-Verbose works but doesn't appear by default + # Unless user sets $VerbosePreference="Continue" + + # Bump the version number of the build + Write-Verbose "[inc_version] Updating $gameIniFile" + + $versionString = $gameIni["/Script/EngineSettings.GeneralProjectSettings"].ProjectVersion + Write-Verbose "[version++] Current version is $versionString" + + # Regex features: + # - Can read 2-4 version components but will pad with 0s up to 4 when writing + # - captures pre- and post-fix text and retains + $regex = "([^\d]*)(\d+)\.(\d+)(?:\.(\d+))?(?:\.(\d+))?(.*)" + $matches = $versionString | Select-String -Pattern $regex + # 1 = prefix + # 2-5 = version number components + # 6 = postfix + if (($matches.Matches.Count -gt 0) -and ($matches.Matches[0].Groups.Count -eq 7)) { + $prefix = $matches.Matches[0].Groups[1].Value + $postfix = $matches.Matches[0].Groups[6].Value + + $intversions = $matches.Matches[0].Groups[2..5] | ForEach-Object { + if ($_.Value -ne "") { + [int]$_.Value + } else { + # We fill in the version numbers to 4 digits always + 0 + } + + } + + if ($major) { + $intversions[0]++ + } elseif ($minor) { + $intversions[1]++ + } elseif ($patch) { + $intversions[2]++ + } else { + $intversions[3]++ + } + $newver = "$prefix$($intversions[0]).$($intversions[1]).$($intversions[2]).$($intversions[3])$postfix" + Write-Verbose "[version++] Bumping version to $newver" + + if ($dryrun) { + Write-Verbose "[version++] dryrun: not changing $gameIniFile" + } else { + $gameIni["/Script/EngineSettings.GeneralProjectSettings"].ProjectVersion = $newver + Out-IniFile -Force -InputObject $gameIni -FilePath $gameIniFile + Write-Verbose "[version++] Success! Version is now $newver" + } + + return "$newver" + + } else { + throw "[version++] Error: unable to read current version" + } +} + diff --git a/packageconfig_template.json b/packageconfig_template.json new file mode 100644 index 0000000..55166ec --- /dev/null +++ b/packageconfig_template.json @@ -0,0 +1,51 @@ +{ + "OutputDir": "/Path/To/Output/Parent/Dir", + "ZipDir": "/Path/To/Zipped/Releases/Folder", + + "Target": "GameName", + "CookAllMaps": true, + "MapsIncluded": [ + ], + "MapsExcluded": [ + ], + "UsePak": true, + "CompressPak": false, + + + "Variants": [ + { + "Name": "PublicWin64SteamBuild", + "Platform": "Win64", + "Configuration": "Shipping", + "SteamAppId": "YourSteamAppId", + "SteamDepotId": "YourWindowsDepotId", + "Zip": false, + "ExtraBuildArguments": "-EnableSteamworks" + }, + { + "Name": "PublicWin64Build", + "Platform": "Win64", + "Configuration": "Shipping", + "ItchAppId": "itch-user/app-name", + "ItchChannel": "win64", + "Zip": false + }, + { + "Name": "PrivateWin64Build", + "Platform": "Win64", + "Configuration": "Development", + "ItchAppId": "itch-user/private-app-name", + "ItchChannel": "win64-dev", + "Zip": true, + "ExtraBuildArguments": "-Foo=Bar -Something" + } + ], + + "DefaultVariants": [ + "PrivateWin64Build" + ], + + "SteamLogin": "YourSteamReleaseUser" +} + + diff --git a/ue4-package.ps1 b/ue4-package.ps1 new file mode 100644 index 0000000..a016a6f --- /dev/null +++ b/ue4-package.ps1 @@ -0,0 +1,170 @@ +[CmdletBinding()] # Fail on unknown args +param ( + [string]$src, + [switch]$major = $false, + [switch]$minor = $false, + [switch]$patch = $false, + [switch]$hotfix = $false, + # Don't incrememnt version + [switch]$noversionbump = $false, + # Force move tag + [switch]$forcetag = $false, + # Name of variant to build (optional, uses DefaultVariants from packageconfig.json if unspecified) + [array]$variant, + # Testing mode; skips clean checks, tags + [switch]$test = $false, + # Dry-run; does nothing but report what *would* have happened + [switch]$dryrun = $false, + [switch]$help = $false +) + +. $PSScriptRoot\inc\packageconfig.ps1 +. $PSScriptRoot\inc\projectversion.ps1 + + +function Write-Usage { + Write-Output "Steve's UE4 packaging tool" + Write-Output "Usage:" + Write-Output " ue4-package.ps1 [-src:sourcefolder] [-major|-minor|-patch|-hotfix] [-keepversion] [-force] [-variant=VariantName] [-test] [-dryrun]" + Write-Output " " + Write-Output " -src : Source folder (current folder if omitted), must contain buildconfig.json" + Write-Output " -major : Increment major version i.e. [x++].0.0.0" + Write-Output " -minor : Increment minor version i.e. x.[x++].0.0" + Write-Output " -patch : Increment patch version i.e. x.x.[x++].0 (default)" + Write-Output " -hotfix : Increment hotfix version i.e. x.x.x.[x++]" + Write-Output " -keepversion : Keep current version number, doesn't tag unless -forcetag" + Write-Output " -forcetag : Move any existing version tag" + Write-Output " -variant=Name : Build only a named variant instead of DefaultVariants from packageconfig.json" + Write-Output " -test : Testing mode, separate builds, allow dirty working copy" + Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" + Write-Output " -help : Print this help" +} + +if ($src.Length -eq 0) { + $src = "." + Write-Verbose "-src not specified, assuming current directory" +} + +# Import config +$config = Read-Package-Config -srcfolder:$src + +$ErrorActionPreference = "Stop" + +Write-Output "~-~-~ UE4 Packaging Helper Start ~-~-~" + +if ($help) { + Write-Usage + Exit 0 +} + +if ($test) { + Write-Output "TEST MODE: No tagging, version bumping" +} + +if (([bool]$major + [bool]$minor + [bool]$patch + [bool]$hotfix) -gt 1) { + Write-Output "ERROR: Can't set more than one of major/minor/patch/hotfix at the same time!" + Print-Usage + Exit 5 +} +if (($major -or $minor -or $patch -or $hotfix) -and $keepversion) { + Write-Output "ERROR: Can't set keepversion at the same time as major/minor/patch/hotfix!" + Print-Usage + Exit 5 +} + +# Detect Git +if ($src -ne ".") { Push-Location $src } +$isGit = Test-Path ".git" +if ($src -ne ".") { Pop-Location } + +# Check working copy is clean (Git only) +if (-not $test -and $isGit) { + if ($src -ne ".") { Push-Location $src } + + if (Test-Path ".git") { + git diff --no-patch --exit-code + if ($LASTEXITCODE -ne 0) { + Write-Output "Working copy is not clean (unstaged changes)" + if ($dryrun) { + Write-Output "dryrun: Continuing but this will fail without -dryrun" + } else { + Exit $LASTEXITCODE + } + } + git diff --no-patch --cached --exit-code + if ($LASTEXITCODE -ne 0) { + Write-Output "Working copy is not clean (staged changes)" + if ($dryrun) { + Write-Output "dryrun: Continuing but this will fail without -dryrun" + } else { + Exit $LASTEXITCODE + } + } + } + if ($src -ne ".") { Pop-Location } +} + +Write-Output "" +Write-Output "Package configuration:" +Write-Output $config + +try { + if (([bool]$major + [bool]$minor + [bool]$patch + [bool]$hotfix) -eq 0) { + $patch = $true + } + $mainver = $null + if ($keepversion) { + $mainver = Get-Project-Version $src + } else { + # Bump up version, passthrough options + try { + $mainver = Increment-Project-Version -srcfolder:$src -major:$major -minor:$minor -patch:$patch -hotfix:$hotfix -dryrun:$dryrun + if (-not $dryrun -and $isGit) { + if ($src -ne ".") { Push-Location $src } + + $verIniFile = Get-Project-Version-Ini-Filename + git add "$($verIniFile)" + if ($LASTEXITCODE -ne 0) { Exit $LASTEXITCODE } + git commit -m "Version bump to $mainver" + if ($LASTEXITCODE -ne 0) { Exit $LASTEXITCODE } + + if ($src -ne ".") { Pop-Location } + } + } + catch { + Write-Output $_.Exception.Message + Exit 6 + } + } + # Keep test builds separate + if ($test) { + $mainver = "$mainver-test" + } + Write-Output "Next version will be: $mainver" + + # For tagging release + # We only need to grab the main version once + $forcearg = "" + if ($forcetag) { + $forcearg = "-f" + } + if (-not $test -and -not $dryrun) { + if ($src -ne ".") { Push-Location $src } + git tag $forcearg -a $mainver -m "Automated release tag" + if ($LASTEXITCODE -ne 0) { Exit $LASTEXITCODE } + if ($src -ne ".") { Pop-Location } + } + + + # TODO: actually package something! + +} +catch { + Write-Output $_.Exception.Message + Exit 9 +} + + + + +Write-Output "~-~-~ UE4 Packaging Helper Completed OK ~-~-~"