From 56378f630c5655780ec9db25467117f119581aa1 Mon Sep 17 00:00:00 2001 From: Lothar Buchholz Date: Sun, 1 Nov 2026 16:30:17 +0100 Subject: [PATCH] - restored release script and made adjustments to release process --- .pkgmeta | 1 + .release/README.md | 222 ++- .release/release.sh | 3441 ++++++++++++++++++++++++++++++++++++++++++ .run/Release.run.xml | 19 + Grichelde.toc | 5 +- 5 files changed, 3639 insertions(+), 49 deletions(-) create mode 100644 .release/release.sh create mode 100644 .run/Release.run.xml diff --git a/.pkgmeta b/.pkgmeta index 93192e5..11e3bf7 100644 --- a/.pkgmeta +++ b/.pkgmeta @@ -23,6 +23,7 @@ embedded-libraries: Libs/AceLocale-3.0: https://repos.wowace.com/wow/ace3/trunk/AceLocale-3.0 ignore: + - out - twitch - GricheldeTest.lua diff --git a/.release/README.md b/.release/README.md index 63330c1..23c0d00 100644 --- a/.release/README.md +++ b/.release/README.md @@ -3,7 +3,7 @@ __release.sh__ generates an addon zip file from a Git, SVN, or Mercurial checkout. -__release.sh__ works by creating a new project directory (*.release* by +__release.sh__ works by creating a new project directory (`.release` by default), copying files from the checkout into the project directory, checking out external repositories then copying their files into the project directory, then moves subdirectories into the project root. The project directory is then @@ -20,12 +20,12 @@ since that tag. For a full example workflow, please check out the [wiki page](https://github.com/BigWigsMods/packager/wiki/GitHub-Actions-workflow). -### Example using [options](#Usage) +### Example using [options](#usage) ```yaml -- uses: BigWigsMods/packager@v1 +- uses: BigWigsMods/packager@v2 with: - args: -g classic -m .pkgmeta-classic + args: -p 1234 -w 5678 -a he54k6bL ``` ## Customizing the build @@ -54,9 +54,13 @@ __release.sh__ can read a __.pkgmeta__ file and supports the following directives. See the [wiki page](https://github.com/BigWigsMods/packager/wiki/Preparing-the-PackageMeta-File) for more info. -- *externals* (Git, SVN, and Mercurial) Caveats: An external's .pkgmeta is only - parsed for ignore and externals will not have localization keywords replaced. +- *externals* (Git, SVN, Mercurial, and zip archives) Caveats: An external's + .pkgmeta is only parsed for ignore and externals will not have localization + keywords replaced. If zip and the archive holds a single top-level folder, the + contents of that folder are used. - *ignore* +- *plain-copy* +- *license-output* - *changelog-title* - *manual-changelog* - *move-folders* @@ -65,6 +69,8 @@ for more info. uploaded to GitHub and attached to a release. Unlike with the CurseForge packager, manually uploaded nolib packages will not be used by the client when users have enabled downloading libraries separately. +- *enable-toc-creation* (defaults to no) Create game type specific TOC files + from your TOC file if you have multiple `## Interface-[Type]:` lines. - *tools-used* - *required-dependencies* - *optional-dependencies* @@ -82,7 +88,7 @@ You can also use a few directives for WoWInterface uploading. changelog in Markdown format to BBCode if you have [pandoc](http://pandoc.org/) installed; otherwise, the manual changelog will be used as-is. If set to `no` when using a generated changelog, Markdown will be used instead of BBCode. - **Note**: Markdown support is experimental and needs to be requested on a + __Note:__: Markdown support is experimental and needs to be requested on a per-project basis. ### String replacements @@ -115,6 +121,10 @@ for more info. - *@file-timestamp@* - *@project-timestamp@* - *@project-version@* +- *@build-date@* +- *@build-date-iso@* +- *@build-date-integer@* +- *@build-timestamp@* ### Build type keywords @@ -129,10 +139,10 @@ Supported keywords and when the code block will run: - `debug`: never. Code will only run when using an unpackaged source. - `do-not-package`: never. Same as `debug` except removed from the packaged file. -- `no-lib-strip`: _(not supported in Lua files)_ in any build other than a +- `no-lib-strip`: *(not supported in Lua files)* in any build other than a *nolib* build. -- `retail`,`version-retail`,`version-classic`,`version-bcc`: based on game - version. +- `retail`,`version-retail`,`version-classic`,`version-bcc`,`version-wrath`, + `version-cata`, `version-mists`: based on game version. `do-not-package` is a bit special. Everything between the tags, including the tags themselves, will always be removed from the packaged file. This will cause @@ -142,7 +152,7 @@ Lua files surrounding debugging functions and other code that end users should never see or execute. All keywords except `do-not-package` can be prefixed with `non-` to inverse the -logic. When doing this, the keywords should start and end a **block comment** +logic. When doing this, the keywords should start and end a __block comment__ as shown below. More examples are available on the [wiki page](https://github.com/BigWigsMods/packager/wiki/Repository-Keyword-Substitutions#debug-replacements). @@ -157,7 +167,7 @@ turn into `--@non-keyword@` and `--@end-non-keyword@`. #### In XML files -**Note:** XML doesn't allow nested comments so make sure not to nest keywords. +__Note:__ XML doesn't allow nested comments so make sure not to nest keywords. If you need to nest keywords, you can do so in the TOC instead. `` and `` @@ -172,7 +182,7 @@ The lines with `#@keyword@` and `#@end-keyword@` get removed, as well as every line in-between. The lines with `#@non-keyword@` and `#@end-non-keyword@` get removed, as well as -removing a '# ' at the beginning of each line in-between. +removing a '# ' (note the space) at the beginning of each line in-between. ### Changing the file name @@ -204,54 +214,145 @@ on the build type: `{classic}` has some additional magic: -1. It will show as the non-retail build type, so either `-classic` or `-bcc`. -2. It will not be shown if "-classic" or "-bcc" is in the project version. +1. It will show as the non-retail build type, so `-classic`, `-bcc`, `-wrath`, + `-cata`, or `-mists`. +2. It will not be shown if `-classic`, `-bcc`, `-wrath`, `-cata`, or `-mists` + is in the project version (tag). 3. If it is included in the file name (it is by default) and #2 does not apply, - it will also be appended to the file label (i.e., the name shown on - CurseForge). + it will also be appended to the file label (i.e., the name shown in the file + list on CurseForge). ## Building for multiple game versions -__release.sh__ needs to know what version of World of Warcraft the package is -targeting. This is normally automatically detected using the `## Interface:` -line of the addon's TOC file. +__release.sh__ automatically detects what game version(s) the addon supports. +You only need to run a build once and the file will be tagged with the +appropriate versions when uploaded. -If your addon supports both retail and classic in the same branch, you can use -multiple `## Interface-Type:` lines in your TOC file. Only one `## Interface:` -line will be included in the packaged TOC file based on the targeted game -version. +For builds with multiple game types, you won't be able to use [build version keywords](#build-type-keywords) +(e.g., `@version-retail@` ... `@end-version-retail@`) in Lua files for +controlling what code blocks execute based on the build version, you need to +switch to plain old Lua control statements. Fortunately, there are some +[constants](https://warcraft.wiki.gg/wiki/WOW_PROJECT_ID) set by Blizzard you +can use for this. If you use these keywords in XML files, you will have to +reorganize your includes in the appropriate TOC files. + +### Multiple Interface values + +You can specify multiple interface versions delimited by commas. If you don't +set an interface version that matches the game client, then the addon is flagged +out-of-date. ```toc -## Interface: 90005 -## Interface-Retail: 90005 -## Interface-Classic: 11306 -## Interface-BCC: 20501 +## Interface: 11509, 20506, 30405, 38002, 40402, 50504, 120100, 16001 ``` -You specify what version of the game you're targeting with the `-g` switch. You -can use a specific version (`release.sh -g 1.13.6`) or you can use the game type -(`release.sh -g classic`). Using a game type will set the game version based on -the appropriate TOC `## Interface` value. +The above example would mark an addon compatible with the latest version of all +client flavors and Forever Beta. -You can also set multiple specific versions as a comma delimited list using the -`-g` switch (`release.sh -g 1.13.6,2.5.1,9.0.5`). This will still only build -one package, with the the last version listed used as the target version for -the build. +That said, just because you *can* include a bunch of interface versions doesn't +mean you *should* start adding upcoming versions you haven't tested your addon +against. -**Setting multiple versions is not recommended!** The addon will always be -marked "Out of date" in-game for versions that do not match the TOC interface -value for the last version set. So even if you don't need any special file -processing, it will always be best to run the packager multiple times so the TOC -interface value is correct for each game version. +### Multiple TOC files + +You can create [multiple TOC files](https://warcraft.wiki.gg/wiki/TOC_format#Naming), +one for each supported game type, and __release.sh__ will use them to set the +build's game version. + +### Single TOC file + +Most clients support a variety of directives to allow conditional loading of the +TOC file using [Loading conditions](https://warcraft.wiki.gg/wiki/TOC_format#Loading_conditions) +or each individual listed file with [Inline directives](https://warcraft.wiki.gg/wiki/TOC_format#Inline_directives). + +Example from the [wiki](https://warcraft.wiki.gg/wiki/TOC_format) (mash up of +variables, loading conditions, inline directives, and multiple values): + +```toc +## Interface: 11509, 20506, 30405, 38002, 40402, 50504, 120100, 16001 +## Title: My Cool AddOn +## Title: My Cool AddOn in Classic [AllowLoadGameType classic] +## Notes: My addon is working in [Game]. +## AllowLoadGameType: standard, classic +## ExcludeLoadGameType: titan, wrath + +# This will load "Mainline\File.lua" or "Classic\File.lua" +# as appropriate for the client. +[Family]\File.lua + +# This will load "Standard\File.lua", "Mists\File.lua", "Cata\File.lua", ... +# as appropriate for the client. +[Game]\File.lua + +# This will load "Localization\enUS.lua", "Localization\frFR.lua", ... +# as appropriate for the client text locale. +Localization\[TextLocale].lua + +# This will only be loaded on Standard (retail). +StandardOnly.lua [AllowLoadGameType standard] + +# This will only be loaded on Camelot (forever). +ForeverOnly.lua [AllowLoadGameType camelot][ExcludeLoadGameType standard, classic] + +# This will only be loaded in Vanilla or TBC. +VanillaOrTBC.lua [AllowLoadGameType vanilla, tbc] + +# This will only be loaded under English or French client locales. +EnglishOrFrenchOnly.lua [AllowLoadTextLocale enUS, frFR] +``` + +__release.sh__ has provided quality of life features to make packaging an addon +for multiple versions easier, but with the TOC features Blizzard has added, most +of the versioning "magic" (build keywords, splitting, multiple packages) is no +longer necessary! + +#### Using TOC file creation (splitting) + +__release.sh__ can support multiple game versions with the use of additional +`## Interface-[Type]` lines in your TOC file. + +```toc +## Interface: 120100 +## Interface-Classic: 11509 +## Interface-Mists: 50504 +``` + +When using multiple `## Interface-[Type]` lines in a single TOC file, you +can use the `-S` command line option or add `enable-toc-creation: yes` to your +`.pkgmeta` file to automatically generate game type specific TOC files using +your existing preprocessing logic. The fallback TOC file will use the base +interface value as it's version. + +For each `## Interface-[Type]` line, a new TOC file is created. In the above +example, __release.sh__ would create `MyAddon_Vanilla.toc` and +`MyAddon_Mists.toc`, based on `MyAddon.toc` applying each game type's processing +logic and rewriting the interface version and also copy `MyAddon.toc` processed +as retail. You can also not include a fallback TOC file to prevent the addon +from displaying for unsupported versions by not including a base interface +value. + +### Single game version + +You can specify what version of the game you're targeting with the `-g` switch. +As the game officially supports multiple game versions, manually setting the +version should only be used if you have a specific reason for creating and +uploading multiple packages. + +If you specify a single game type (`release.sh -g classic`), the game version +will be set based on the appropriate TOC `## Interface-[Type]` value. You can +also completely override version detection by passing a version number +(`release.sh -g 1.15.9`) or a list of versions (`release.sh -g "5.5.4,1.15.9"`). ## Building locally The recommended way to include __release.sh__ in a project is to: -1. Create a *.release* subdirectory in your top-level checkout. -2. Copy __release.sh__ into the *.release* directory. -3. Ignore the *.release* subdirectory in __.gitignore__. -4. Run __release.sh__. +1. Create a `.release` subdirectory in the root of your checkout. +2. Ignore the `.release` subdirectory in your `.gitignore`. +3. Copy __release.sh__ into the `.release` directory. +4. (Optionally) Create a `.env` file in the `.release` directory filled with + your upload secrets. (KEY=value pairs each on a new line) +5. Run __release.sh__. ## Usage @@ -264,6 +365,7 @@ Usage: release.sh [options] -L Only do @localization@ keyword replacement (skip upload to CurseForge). -o Keep existing package directory, overwriting its contents. -s Create a stripped-down "nolib" package. + -S Create a package supporting multiple game types from a single TOC file. -u Use Unix line-endings. -z Skip zip file creation. -t topdir Set top-level directory of checkout. @@ -273,20 +375,43 @@ Usage: release.sh [options] -a wago-id Set the project id used on Wago Addons for uploading. (Use 0 to unset the TOC value) -g game-version Set the game version to use for uploading. -m pkgmeta.yaml Set the pkgmeta file to use. - -n package-name Set the package zip file name. Use "-n help" for more info. + -n "{template}" Set the package zip file name and upload label. Use "-n help" for more info. +``` + +```text +Usage: release.sh -n "{template}" + Set the package zip file name and upload file label. There are several string + substitutions you can use to include version control and build type information in + the file name and upload label. + + The default file name is "{package-name}-{project-version}{nolib}{classic}". + The default upload label is "{project-version}{classic}{nolib}". + + To set both, separate with a ":", i.e, "{file template}:{label template}". + If either side of the ":" is blank, the default will be used. Not including a ":" + will set the file name template, leaving upload label as default. + + Tokens: {package-name}{project-revision}{project-hash}{project-abbreviated-hash} + {project-author}{project-date-iso}{project-date-integer}{project-timestamp} + {project-version}{game-type}{release-type} + + Flags: {alpha}{beta}{nolib}{classic} + + Tokens are always replaced with their value. Flags are shown prefixed with a dash + depending on the build type. ``` ### Uploading __release.sh__ uses following environment variables for uploading: -- `CF_API_KEY` - a [CurseForge API token](https://wow.curseforge.com/account/api-tokens), +- `CF_API_TOKEN` - a [CurseForge API token](https://authors.curseforge.com/#/settings/api-tokens), required for the CurseForge API to fetch localization and upload files. - `WOWI_API_TOKEN` - a [WoWInterface API token](https://www.wowinterface.com/downloads/filecpl.php?action=apitokens), required for uploading to WoWInterface. - `WAGO_API_TOKEN` - a [Wago Addons API token](https://addons.wago.io/account/apikeys), required for uploading to Wago Addons. -- `GITHUB_OAUTH` - a [GitHub personal access token](https://github.com/settings/tokens), +- `GH_API_TOKEN` - a [GitHub personal access token](https://github.com/settings/personal-access-tokens), required for uploading to GitHub. __release.sh__ will attempt to load environment variables from a `.env` file in @@ -300,6 +425,7 @@ environment provided the following are available: - bash >= 4.3 - awk +- grep - sed - curl - zip diff --git a/.release/release.sh b/.release/release.sh new file mode 100644 index 0000000..2894bc3 --- /dev/null +++ b/.release/release.sh @@ -0,0 +1,3441 @@ +#!/usr/bin/env bash + +# release.sh generates an addon zip file from a Git, SVN, or Mercurial checkout. +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# 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 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. +# +# For more information, please refer to + +# Global shellcheck excludes: +# SC2295: Expansions inside ${..} need to be quoted separately, otherwise they will match as a pattern. +# SC2030: Modification of var is local (to subshell caused by pipeline). +# SC2031: var was modified in a subshell. That change might be lost. +# SC2317: Command appears to be unreachable. +# SC2329: This function is never invoked. +# SC1091: Not following, file does not exist. +# shellcheck disable=SC2295,SC2030,SC2031,SC2317,SC2329,SC1091 + +## USER OPTIONS + +# Secrets for uploading +cf_token= +github_token= +wowi_token= +wago_token= + +# Variables set via command-line options +slug= +addonid= +wagoid= +topdir= +releasedir= +overwrite= +nolib= +split= +line_ending="dos" +skip_copying= +skip_externals= +skip_localization= +skip_zipfile= +skip_upload= +skip_cf_upload= +pkgmeta_file= +game_version= +game_type= +file_type= +file_template="{package-name}-{project-version}{nolib}{classic}" +label_template="{project-version}{classic}{nolib}" + +wowi_markup="bbcode" + +## END USER OPTIONS + +if [[ ${BASH_VERSINFO[0]} -lt 4 ]] || [[ ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 3 ]]; then + echo "ERROR! bash version 4.3 or above is required. Your version is ${BASH_VERSION}." >&2 + exit 1 +fi + +# Game versions for uploading +declare -A game_flavor=( ["retail"]="retail" ["classic"]="classic" ["bcc"]="bcc" ["mainline"]="retail" ["tbc"]="bcc" ["vanilla"]="classic" ["wrath"]="wrath" ["wotlkc"]="wrath" ["cata"]="cata" ["mists"]="mists" ["titan"]="titan" ["forever"]="forever" ["camelot"]="forever" ) + +declare -A game_type_version=() # type -> version (: delim) +declare -A game_type_interface=() # type -> toc (: delim) +declare -A si_game_type_interface_all=() # type -> interface value (last file) (values from ## Interface*:) +declare -A si_game_type_interface=() # type -> game type interface value (last file) (values from ## Interface-Type:) +declare si_game_root_interface # base interface value (last file) +declare -A toc_interfaces=() # path -> si_game_type_interface_all (: delim) +declare -A toc_root_interface=() # path -> base interface value (values from ## Interface:) +declare -A toc_root_paths=() # path -> directory name +declare -a toc_paths=() # toc path in order of processing + +# Script return code +exit_code=0 + +retry() { + local result=0 + local count=1 + local max=10 + while [[ $count -le "$max" ]]; do + [[ $result -ne 0 ]] && { + echo -e "\033[01;31mRetrying (${count}/${max})\033[0m" >&2 + } + "$@" && { result=0 && break; } || result="$?" + count="$((count + 1))" + sleep 3 + done + return "$result" +} + +retry_svn_checkout() { + local repo="$1" + local path="$2" + local result=0 + local count=1 + local max=5 + while [[ $count -le "$max" ]]; do + [[ $result -ne 0 ]] && { + echo -e "\033[01;31mRetrying (${count}/${max})\033[0m" >&2 + rm -rf "$path" # an aborted checkout can leave a .svn directory with a locked state + } + svn checkout -q "$repo" "$path" --force && { result=0 && break; } || result="$?" + count="$((count + 1))" + sleep 3 + done + return "$result" +} + +# Fetch a file from an svn repository. The file not existing is not an error. +svn_cat_optional() { + local url="$1" + local path="$2" + local error + if ! error=$( svn cat "$url" 2>&1 > "$path" ); then + rm -f "$path" + # 160013 is "path not found" + if [[ $error != *"160013"* ]]; then + echo "$error" >&2 + return 1 + fi + fi +} + +# Escape a string for use in sed substitutions. +escape_substr() { + local s="$1" + s=${s//\\/\\\\} + s=${s//\//\\/} + s=${s//&/\\&} + echo "$s" +} + +# File name templating +filename_filter() { + local classic alpha beta invalid="_" + [ -n "$skip_invalid" ] && invalid="&" + if [[ -n $game_type ]] && [[ "$game_type" != "retail" ]] && \ + [[ "$game_type" != "classic" || "${si_project_version,,}" != *"-classic"* ]] &&\ + [[ "$game_type" != "bcc" || "${si_project_version,,}" != *"-bcc"* ]] &&\ + [[ "$game_type" != "wrath" || "${si_project_version,,}" != *"-wrath"* ]] &&\ + [[ "$game_type" != "cata" || "${si_project_version,,}" != *"-cata"* ]] &&\ + [[ "$game_type" != "mists" || "${si_project_version,,}" != *"-mists"* ]] &&\ + [[ "$game_type" != "titan" || "${si_project_version,,}" != *"-titan"* ]] &&\ + [[ "$game_type" != "forever" || "${si_project_version,,}" != *"-forever"* ]] + then + # only append the game type if the tag doesn't include it + classic="-$game_type" + fi + [ "$file_type" == "alpha" ] && alpha="-alpha" + [ "$file_type" == "beta" ] && beta="-beta" + sed \ + -e "s/{package-name}/$( escape_substr "$package" )/g" \ + -e "s/{project-revision}/$si_project_revision/g" \ + -e "s/{project-hash}/$si_project_hash/g" \ + -e "s/{project-abbreviated-hash}/$si_project_abbreviated_hash/g" \ + -e "s/{project-author}/$( escape_substr "$si_project_author" )/g" \ + -e "s/{project-date-iso}/$si_project_date_iso/g" \ + -e "s/{project-date-integer}/$si_project_date_integer/g" \ + -e "s/{project-timestamp}/$si_project_timestamp/g" \ + -e "s/{project-version}/$( escape_substr "$si_project_version" )/g" \ + -e "s/{game-type}/${game_type}/g" \ + -e "s/{release-type}/${file_type}/g" \ + -e "s/{alpha}/${alpha}/g" \ + -e "s/{beta}/${beta}/g" \ + -e "s/{nolib}/${nolib:+-nolib}/g" \ + -e "s/{classic}/${classic}/g" \ + -e "s/\([^A-Za-z0-9._!-]\)/${invalid}/g" \ + <<< "$1" +} + +toc_filter() { + local keyword="$1" + local remove="$2" + if [ -z "$remove" ]; then + # "active" build type: remove comments (keep content), remove non-blocks (remove all) + sed \ + -e "/#@\(end-\)\{0,1\}${keyword}@/d" \ + -e "/#@non-${keyword}@/,/#@end-non-${keyword}@/d" + else + # "non" build type: remove blocks (remove content), uncomment non-blocks (remove tags) + sed \ + -e "/#@${keyword}@/,/#@end-${keyword}@/d" \ + -e "/#@non-${keyword}@/,/#@end-non-${keyword}@/s/^#[[:blank:]]\{1,\}//" \ + -e "/#@\(end-\)\{0,1\}non-${keyword}@/d" + fi +} + +toc_to_type() { + local toc_version="$1" + local -n game_type="$2" || return 1 + case $toc_version in + 11???) game_type="classic" ;; + 16???) game_type="forever" ;; + 20???) game_type="bcc" ;; + 30???) game_type="wrath" ;; + 40???) game_type="cata" ;; + 50???) game_type="mists" ;; + 380??) game_type="titan" ;; + *) game_type="retail" + esac + # return game_type +} + +toc_to_file_type() { + local toc_version="$1" + local -n game_type="$2" || return 1 + case $toc_version in + 11???) game_type="classic" ;; + 16???) game_type="forever" ;; + 20???) game_type="bcc" ;; + 30???) game_type="wrath" ;; + 40???) game_type="cata" ;; + 50???) game_type="mists" ;; + 380??) game_type="wrath" ;; + *) game_type="retail" + esac + # return game_type +} + +is_toc_multiple_types() { + local toc_version="$1" + local toc_game_type toc_version_game_type + IFS=':,' read -ra V <<< "$toc_version" + toc_to_type "${V[0]}" "toc_game_type" + for i in "${V[@]}"; do + toc_to_type "$i" "toc_version_game_type" + [[ $toc_game_type != "$toc_version_game_type" ]] && return 0 + done + return 1 +} + + +# Process command-line options +usage() { + cat <<-'EOF' >&2 + Usage: release.sh [options] + -c Skip copying files into the package directory. + -d Skip uploading. + -e Skip checkout of external repositories. + -l Skip @localization@ keyword replacement. + -L Only do @localization@ keyword replacement (skip upload to CurseForge). + -o Keep existing package directory, overwriting its contents. + -s Create a stripped-down "nolib" package. + -S Create a package supporting multiple game types from a single TOC file. + -u Use Unix line-endings. + -z Skip zip file creation. + -t topdir Set top-level directory of checkout. + -r releasedir Set directory containing the package directory. Defaults to "$topdir/.release". + -p curse-id Set the project id used on CurseForge for localization and uploading. (Use 0 to unset the TOC value) + -w wowi-id Set the addon id used on WoWInterface for uploading. (Use 0 to unset the TOC value) + -a wago-id Set the project id used on Wago Addons for uploading. (Use 0 to unset the TOC value) + -g game-version Set the game version to use for uploading. + -m pkgmeta.yaml Set the pkgmeta file to use. + -n "{template}" Set the package zip file name and upload label. Use "-n help" for more info. + EOF +} + +OPTIND=1 +while getopts ":celLzusSop:dw:a:r:t:g:m:n:" opt; do + case $opt in + c) skip_copying="true" ;; # Skip copying files into the package directory + z) skip_zipfile="true" ;; # Skip creating a zip file + e) skip_externals="true" ;; # Skip checkout of external repositories + l) skip_localization="true" ;; # Skip @localization@ keyword replacement + L) skip_cf_upload="true" ;; # Skip uploading to CurseForge + d) skip_upload="true" ;; # Skip uploading + u) line_ending="unix" ;; # Use LF instead of CRLF as the line ending for all text files + o) overwrite="true" ;; # Don't delete existing directories in the release directory + p) slug="$OPTARG" ;; # Set CurseForge project id + w) addonid="$OPTARG" ;; # Set WoWInterface addon id + a) wagoid="$OPTARG" ;; # Set Wago Addons project id + r) releasedir="$OPTARG" ;; # Set the release directory + t) # Set the top-level directory of the checkout + if [ ! -d "$OPTARG" ]; then + echo "Invalid argument for option \"-t\" - Directory \"$OPTARG\" does not exist." >&2 + usage + exit 1 + fi + topdir="$OPTARG" + ;; + s) # Create a nolib package without externals + nolib="true" + skip_externals="true" + ;; + S) split="true" ;; # Split TOC + g) # Set the game type or version + OPTARG="${OPTARG,,}" + case "$OPTARG" in + retail|classic|bcc|wrath|cata|mists|titan|forever) game_type="$OPTARG" ;; # game_version from toc + mainline) game_type="retail" ;; + camelot) game_type="forever" ;; + *) + # Set game version (x.y.z) + # Build game type set from the last value if a list + IFS=',' read -ra V <<< "$OPTARG" + for i in "${V[@]}"; do + if [[ ! "$i" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)[a-z]?$ ]]; then + echo "Invalid argument for option \"-g\" ($i)" >&2 + usage + exit 1 + fi + if [[ ${BASH_REMATCH[1]} == "1" ]]; then + if [[ ${BASH_REMATCH[2]} == 6[0-9] ]]; then + game_type="forever" + else + game_type="classic" + fi + elif [[ ${BASH_REMATCH[1]} == "2" ]]; then + game_type="bcc" + elif [[ ${BASH_REMATCH[1]} == "3" ]]; then + if [[ ${BASH_REMATCH[2]} == "80" ]]; then + game_type="titan" + else + game_type="wrath" + fi + elif [[ ${BASH_REMATCH[1]} == "4" ]]; then + game_type="cata" + elif [[ ${BASH_REMATCH[1]} == "5" ]]; then + game_type="mists" + else + game_type="retail" + fi + if [[ -n ${game_type_version[$game_type]} ]]; then + game_type_version[$game_type]="${game_type_version[$game_type]}:$i" + game_type_interface[$game_type]="${game_type_interface[$game_type]}:$( printf "%d%02d%02d" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" )" + else + game_type_version[$game_type]="$i" + game_type_interface[$game_type]=$( printf "%d%02d%02d" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" ) + fi + done + if [[ ${#game_type_version[@]} -gt 1 ]]; then + game_type= + fi + game_version="$OPTARG" + esac + ;; + m) # Set the pkgmeta file + if [ ! -f "$OPTARG" ]; then + echo "Invalid argument for option \"-m\" - File \"$OPTARG\" does not exist." >&2 + usage + exit 1 + fi + pkgmeta_file="$OPTARG" + ;; + n) # Set the package file name + if [ "$OPTARG" = "help" ]; then + cat <<-'EOF' >&2 + Usage: release.sh [options] + Set the package zip file name and upload file label. There are several string + substitutions you can use to include version control and build type information in + the file name and upload label. + + The default file name is "{package-name}-{project-version}{nolib}{classic}". + The default upload label is "{project-version}{classic}{nolib}". + + To set both, separate with a ":", i.e, "{file template}:{label template}". + If either side of the ":" is blank, the default will be used.Not including a ":" + will set the file name template, leaving upload label as default. + + Tokens: {package-name}{project-revision}{project-hash}{project-abbreviated-hash} + {project-author}{project-date-iso}{project-date-integer}{project-timestamp} + {project-version}{game-type}{release-type} + + Flags: {alpha}{beta}{nolib}{classic} + + Tokens are always replaced with their value. Flags are shown prefixed with a dash + depending on the build type. + EOF + exit 0 + fi + if skip_invalid=true filename_filter "$OPTARG" | grep -q '[{}]'; then + tokens=$( skip_invalid=true filename_filter "$OPTARG" | sed -e '/^[^{]*{\|}[^{]*{\|}[^{]*/s//}{/g' -e 's/^}\({.*}\){$/\1/' ) + echo "Invalid argument for option \"-n\" - Invalid substitutions: $tokens" >&2 + exit 1 + fi + file_template=${OPTARG%%:*} + if [ -z "$file_template" ]; then + file_template="{package-name}-{project-version}{nolib}{classic}" + fi + label_template=${OPTARG##*:} + if [ -z "$label_template" ]; then + label_template="{project-version}{classic}{nolib}" + fi + #"{package-name}-{project-version}{nolib}{classic}:{project-version}{classic}{nolib}" + ;; + :) + echo "Option \"-$OPTARG\" requires an argument." >&2 + usage + exit 1 + ;; + \?) + if [ "$OPTARG" = "?" ] || [ "$OPTARG" = "h" ]; then + usage + exit 0 + fi + echo "Unknown option \"-$OPTARG\"" >&2 + usage + exit 1 + ;; + esac +done +shift $((OPTIND - 1)) + +# Set $topdir to top-level directory of the checkout. +if [ -z "$topdir" ]; then + dir=$( pwd ) + if [ -d "$dir/.git" ] || [ -d "$dir/.svn" ] || [ -d "$dir/.hg" ]; then + topdir=. + else + dir=${dir%/*} + topdir=".." + while [ -n "$dir" ]; do + if [ -d "$topdir/.git" ] || [ -d "$topdir/.svn" ] || [ -d "$topdir/.hg" ]; then + break + fi + dir=${dir%/*} + topdir="$topdir/.." + done + if [ ! -d "$topdir/.git" ] && [ ! -d "$topdir/.svn" ] && [ ! -d "$topdir/.hg" ]; then + echo "No Git, SVN, or Hg checkout found." >&2 + exit 1 + fi + fi +fi + +# Handle folding sections in CI logs +start_group() { echo "$1"; } +end_group() { echo; } + +# Check for Travis CI +if [ -n "$TRAVIS" ]; then + # Don't run the packager for pull requests + if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then + echo "Not packaging pull request." + exit 0 + fi + if [ -z "$TRAVIS_TAG" ]; then + # Don't run the packager if there is a tag pending + check_tag=$( git -C "$topdir" tag --points-at HEAD ) + if [ -n "$check_tag" ]; then + echo "Found future tag \"${check_tag}\", not packaging." + exit 0 + fi + # Only package master, classic, or develop + if [ "$TRAVIS_BRANCH" != "master" ] && [ "$TRAVIS_BRANCH" != "classic" ] && [ "$TRAVIS_BRANCH" != "develop" ]; then + echo "Not packaging \"${TRAVIS_BRANCH}\"." + exit 0 + fi + fi + # https://github.com/travis-ci/travis-build/tree/master/lib/travis/build/bash + start_group() { + echo -en "travis_fold:start:$2\\r\033[0K" + # release_timer_id="$(printf %08x $((RANDOM * RANDOM)))" + # release_timer_start_time="$(date -u +%s%N)" + # echo -en "travis_time:start:${release_timer_id}\\r\033[0K" + echo "$1" + } + end_group() { + # local release_timer_end_time="$(date -u +%s%N)" + # local duration=$((release_timer_end_time - release_timer_start_time)) + # echo -en "\\ntravis_time:end:${release_timer_id}:start=${release_timer_start_time},finish=${release_timer_end_time},duration=${duration}\\r\033[0K" + echo -en "travis_fold:end:$1\\r\033[0K" + } +fi + +# Check for GitHub Actions +if [[ -n $GITHUB_ACTIONS ]]; then + # Prevent duplicate builds from multiple pushes + if [[ $GITHUB_EVENT_NAME == "push" && $GITHUB_REF == "refs/heads"* ]]; then + check_tag=$( git -C "$topdir" tag --points-at HEAD ) + if [[ -n $check_tag ]]; then + echo "Found future tag \"${check_tag}\", not packaging." + exit 0 + fi + unset check_tag + fi + start_group() { echo "##[group]$1"; } + end_group() { echo "##[endgroup]"; } +fi + +# Load secrets +if [ -f "$topdir/.env" ]; then + . "$topdir/.env" +elif [ -f ".env" ]; then + . ".env" +fi +[ -z "$cf_token" ] && cf_token=$CF_API_TOKEN +[ -z "$cf_token" ] && cf_token=$CF_API_KEY # deprecated +[ -z "$github_token" ] && github_token=$GH_API_TOKEN +[ -z "$github_token" ] && github_token=$GITHUB_API_TOKEN +[ -z "$github_token" ] && github_token=$GITHUB_OAUTH # deprecated +[ -z "$wowi_token" ] && wowi_token=$WOWI_API_TOKEN +[ -z "$wago_token" ] && wago_token=$WAGO_API_TOKEN + +# Set $releasedir to the directory which will contain the generated addon zipfile. +if [ -z "$releasedir" ]; then + releasedir="$topdir/.release" +fi + +# Set $basedir to the basename of the checkout directory. +basedir=$( cd "$topdir" && pwd ) +case $basedir in + /*/*) basedir=${basedir##/*/} ;; + /*) basedir=${basedir##/} ;; +esac + +# Set $repository_type to "git" or "svn" or "hg". +repository_type= +if [ -d "$topdir/.git" ]; then + repository_type=git +elif [ -d "$topdir/.svn" ]; then + repository_type=svn +elif [ -d "$topdir/.hg" ]; then + repository_type=hg +else + echo "No Git, SVN, or Hg checkout found in \"$topdir\"." >&2 + exit 1 +fi + +# $releasedir must be an absolute path or inside $topdir. +case $releasedir in + /*) ;; + $topdir/*) ;; + *) + echo "The release directory \"$releasedir\" must be an absolute path or inside \"$topdir\"." >&2 + exit 1 + ;; +esac + +# Create the staging directory. +mkdir -p "$releasedir" 2>/dev/null || { + echo "Unable to create the release directory \"$releasedir\"." >&2 + exit 1 +} + +# Expand $topdir and $releasedir to their absolute paths for string comparisons later. +topdir=$( cd "$topdir" && pwd ) +releasedir=$( cd "$releasedir" && pwd ) + +### +### set_info_ returns the following information: +### +si_repo_type= # "git" or "svn" or "hg" +si_repo_dir= # the checkout directory +si_repo_url= # the checkout url +si_tag= # tag for HEAD +si_previous_tag= # previous tag +si_previous_revision= # [SVN|Hg] revision number for previous tag + +si_project_revision= # Turns into the highest revision of the entire project in integer form, e.g. 1234, for SVN. Turns into the commit count for the project's hash for Git. +si_project_hash= # [Git|Hg] Turns into the hash of the entire project in hex form. e.g. 106c634df4b3dd4691bf24e148a23e9af35165ea +si_project_abbreviated_hash= # [Git|Hg] Turns into the abbreviated hash of the entire project in hex form. e.g. 106c63f +si_project_author= # Turns into the last author of the entire project. e.g. ckknight +si_project_date_iso= # Turns into the last changed date (by UTC) of the entire project in ISO 8601. e.g. 2008-05-01T12:34:56Z +si_project_date_integer= # Turns into the last changed date (by UTC) of the entire project in a readable integer fashion. e.g. 2008050123456 +si_project_timestamp= # Turns into the last changed date (by UTC) of the entire project in POSIX timestamp. e.g. 1209663296 +si_project_version= # Turns into an approximate version of the project. The tag name if on a tag, otherwise it's up to the repo. SVN returns something like "r1234", Git returns something like "v0.1-873fc1" + +si_file_revision= # Turns into the current revision of the file in integer form, e.g. 1234, for SVN. Turns into the commit count for the file's hash for Git. +si_file_hash= # Turns into the hash of the file in hex form. e.g. 106c634df4b3dd4691bf24e148a23e9af35165ea +si_file_abbreviated_hash= # Turns into the abbreviated hash of the file in hex form. e.g. 106c63 +si_file_author= # Turns into the last author of the file. e.g. ckknight +si_file_date_iso= # Turns into the last changed date (by UTC) of the file in ISO 8601. e.g. 2008-05-01T12:34:56Z +si_file_date_integer= # Turns into the last changed date (by UTC) of the file in a readable integer fashion. e.g. 20080501123456 +si_file_timestamp= # Turns into the last changed date (by UTC) of the file in POSIX timestamp. e.g. 1209663296 + +si_build_timestamp=$( date "+%s" ) +si_build_date=$( TZ='' printf "%(%Y-%m-%d)T" "$si_build_timestamp" ) +si_build_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_build_timestamp" ) +si_build_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_build_timestamp" ) + +# SVN date helper function +strtotime() { + local value="$1" # datetime string + local format="$2" # strptime string + if [[ "$OSTYPE" == "darwin"* || "$OSTYPE" == *"bsd"* ]]; then # bsd + date -j -f "$format" "$value" "+%s" 2>/dev/null + else # gnu + date -d "$value" +%s 2>/dev/null + fi +} + +set_info_git() { + si_repo_dir="$1" + si_repo_type="git" + si_repo_url=$( git -C "$si_repo_dir" remote get-url origin 2>/dev/null | sed -e 's/^git@\(.*\):/https:\/\/\1\//' ) + if [ -z "$si_repo_url" ]; then # no origin so grab the first fetch url + si_repo_url=$( git -C "$si_repo_dir" remote -v | awk '/(fetch)/ { print $2; exit }' | sed -e 's/^git@\(.*\):/https:\/\/\1\//' ) + fi + + # Populate filter vars. + si_project_hash=$( git -C "$si_repo_dir" show --no-patch --format="%H" 2>/dev/null ) + si_project_abbreviated_hash=$( git -C "$si_repo_dir" show --no-patch --abbrev=7 --format="%h" 2>/dev/null ) + si_project_author=$( git -C "$si_repo_dir" show --no-patch --format="%an" 2>/dev/null ) + si_project_timestamp=$( git -C "$si_repo_dir" show --no-patch --format="%at" 2>/dev/null ) + si_project_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_project_timestamp" ) + si_project_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_project_timestamp" ) + # XXX --depth limits rev-list :\ [ -s "$si_repo_dir/$(git rev-parse --git-dir)/shallow" ] && git -C "$si_repo_dir" fetch --quiet --unshallow --no-tags + # actions/checkout using clone --filter=blob:none instead of their current unholy abomination would be nice + si_project_revision=$( git -C "$si_repo_dir" rev-list --count "$si_project_hash" 2>/dev/null ) + + # Get the tag for the HEAD. + si_previous_tag= + si_previous_revision= + # git describe should use the latest tag if there are multiple, but it doesn't always work that way. + local _si_tag check_tag=() + while IFS='' read -r tag; do check_tag+=( "$tag" ); done < <( git -C "$si_repo_dir" tag --points-at HEAD --sort=-committerdate --sort=-v:refname --sort=-taggerdate ) + if [[ ${#check_tag[@]} -gt 0 ]]; then + # Use the most recent tag + si_tag="${check_tag[0]}" + _si_tag="$si_tag" + + if [[ $GITHUB_REF == "refs/tags/"* && $si_tag != "$GITHUB_REF_NAME" ]]; then + # Git doesn't agree with GitHub Actions, check if there is another local tag that matches + for tag in "${check_tag[@]}"; do + if [[ $tag == "$GITHUB_REF_NAME" ]]; then + si_tag="$tag" + _si_tag="$si_tag" + break + fi + done + fi + else + _si_tag=$( git -C "$si_repo_dir" describe --tags --always --abbrev=7 2>/dev/null ) + si_tag=$( git -C "$si_repo_dir" describe --tags --always --abbrev=0 2>/dev/null ) + fi + # Set $si_project_version to the version number of HEAD. + si_project_version="$si_tag" + # The HEAD is not tagged if the HEAD is several commits past the most recent tag. + if [[ $si_tag == "$si_project_hash" ]]; then + # --abbrev=0 expands out the full sha if there was no previous tag + si_project_version="$_si_tag" + si_previous_tag= + si_tag= + elif [[ $_si_tag != "$si_tag" ]]; then + # not on a tag + si_project_version=$( git -C "$si_repo_dir" describe --tags --abbrev=7 --exclude="*[Aa][Ll][Pp][Hh][Aa]*" 2>/dev/null ) + if [[ -n $si_project_version ]]; then + si_previous_tag=$( git -C "$si_repo_dir" describe --tags --abbrev=0 --exclude="*[Aa][Ll][Pp][Hh][Aa]*" 2>/dev/null ) + else # no previous non-alpha tag + si_project_version="$_si_tag" + si_previous_tag= + fi + si_tag= + else # we're on a tag, just jump back one commit + if [[ ${si_tag,,} != *"beta"* && ${si_tag,,} != *"alpha"* ]]; then + # full release, ignore beta tags + si_previous_tag=$( git -C "$si_repo_dir" describe --tags --abbrev=0 --exclude="*[Aa][Ll][Pp][Hh][Aa]*" --exclude="*[Bb][Ee][Tt][Aa]*" HEAD~ 2>/dev/null ) + else + si_previous_tag=$( git -C "$si_repo_dir" describe --tags --abbrev=0 --exclude="*[Aa][Ll][Pp][Hh][Aa]*" HEAD~ 2>/dev/null ) + fi + fi +} + +set_info_svn() { + si_repo_dir="$1" + si_repo_type="svn" + + local _si_svninfo _si_url _si_revision _si_tag_line _si_tag _si_tag_from_revision _si_timestamp + _si_svninfo=$( retry svn info -r BASE "$si_repo_dir" 2>/dev/null ) + if [[ -z "$_si_svninfo" ]]; then + echo "Unable to read svn repo at $si_repo_dir" >&2 + exit 1 + fi + + si_repo_url=$( awk '/^Repository Root:/ { print $3; exit }' <<< "$_si_svninfo" ) + _si_url=$( awk '/^URL:/ { print $2; exit }' <<< "$_si_svninfo" ) + _si_revision=$( awk '/^Last Changed Rev:/ { print $NF; exit }' <<< "$_si_svninfo" ) + + case ${_si_url#"${si_repo_url}"/} in + tags/*) + # Extract the tag from the URL. + si_tag=${_si_url#"${si_repo_url}"/tags/} + si_tag=${si_tag%%/*} + si_project_revision="$_si_revision" + ;; + *) + # Check if the latest tag matches the working copy revision (/trunk checkout instead of /tags) + _si_tag_line=$( retry svn log --verbose --limit 1 "$si_repo_url/tags" 2>/dev/null | awk '/^ A/ { print $0; exit }' ) + _si_tag=$( echo "$_si_tag_line" | awk '/^ A/ { print $2 }' | awk -F/ '{ print $NF }' ) + # (from /project/trunk:REV) + _si_tag_from_revision=${_si_tag_line##*:} + _si_tag_from_revision=${_si_tag_from_revision%)*} + + if [[ "$_si_tag_from_revision" == "$_si_revision" ]]; then + si_tag="$_si_tag" + si_project_revision=$( retry svn info "$si_repo_url/tags/$si_tag" 2>/dev/null | awk '/^Last Changed Rev:/ { print $NF; exit }' ) + else + # Set $si_project_revision to the highest revision of the project at the checkout path + si_project_revision=$( svn info --recursive "$si_repo_dir" 2>/dev/null | awk '/^Last Changed Rev:/ { print $NF }' | sort -nr | head -n1 ) + fi + ;; + esac + + if [[ -n "$si_tag" ]]; then + si_project_version="$si_tag" + else + si_project_version="r$si_project_revision" + fi + + # Get the previous tag and it's revision + _si_tag=$( retry svn log --verbose --limit 1 "$si_repo_url/tags" -r $((si_project_revision - 1)):1 2>/dev/null | awk '/^ A/ { print $0; exit }' | awk '/^ A/ { print $2 }' | awk -F/ '{ print $NF }' ) + if [[ -n "$_si_tag" ]]; then + si_previous_tag="$_si_tag" + si_previous_revision=$( retry svn info "$si_repo_url/tags/$_si_tag" 2>/dev/null | awk '/^Last Changed Rev:/ { print $NF; exit }' ) + fi + + # Populate filter vars. + si_project_author=$( awk '/^Last Changed Author:/ { print $0; exit }' <<< "$_si_svninfo" | cut -d" " -f4- ) + _si_timestamp=$( awk '/^Last Changed Date:/ { print $4,$5; exit }' <<< "$_si_svninfo" ) + si_project_timestamp=$( strtotime "$_si_timestamp" "%F %T" ) + si_project_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_project_timestamp" ) + si_project_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_project_timestamp" ) + # SVN repositories have no project hash. + si_project_hash= + si_project_abbreviated_hash= +} + +set_info_hg() { + si_repo_dir="$1" + si_repo_type="hg" + si_repo_url=$( hg --cwd "$si_repo_dir" paths -q default ) + if [ -z "$si_repo_url" ]; then # no default so grab the first path + si_repo_url=$( hg --cwd "$si_repo_dir" paths | awk '{ print $3; exit }' ) + fi + + # Populate filter vars. + si_project_hash=$( hg --cwd "$si_repo_dir" log -r . --template '{node}' 2>/dev/null ) + si_project_abbreviated_hash=$( hg --cwd "$si_repo_dir" log -r . --template '{node|short}' 2>/dev/null ) + si_project_author=$( hg --cwd "$si_repo_dir" log -r . --template '{author}' 2>/dev/null ) + si_project_timestamp=$( hg --cwd "$si_repo_dir" log -r . --template '{date}' 2>/dev/null | cut -d. -f1 ) + si_project_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_project_timestamp" ) + si_project_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_project_timestamp" ) + si_project_revision=$( hg --cwd "$si_repo_dir" log -r . --template '{rev}' 2>/dev/null ) + + # Get tag info + si_tag= + # I'm just muddling through revsets, so there is probably a better way to do this + # Ignore tag commits, so v1.0-1 will package as v1.0 + if [ "$( hg --cwd "$si_repo_dir" log -r '.-filelog(.hgtags)' --template '{rev}' 2>/dev/null )" == "" ]; then + _si_tip=$( hg --cwd "$si_repo_dir" log -r 'last(parents(.))' --template '{rev}' 2>/dev/null ) + else + _si_tip=$( hg --cwd "$si_repo_dir" log -r . --template '{rev}' 2>/dev/null ) + fi + si_previous_tag=$( hg --cwd "$si_repo_dir" log -r "$_si_tip" --template '{latesttag}' 2>/dev/null ) + # si_project_version=$( hg --cwd "$si_repo_dir" log -r "$_si_tip" --template "{ ifeq(changessincelatesttag, 0, latesttag, '{latesttag}-{changessincelatesttag}-m{node|short}') }" 2>/dev/null ) # git style + si_project_version=$( hg --cwd "$si_repo_dir" log -r "$_si_tip" --template "{ ifeq(changessincelatesttag, 0, latesttag, 'r{rev}') }" 2>/dev/null ) # svn style + if [ "$si_previous_tag" = "$si_project_version" ]; then + # we're on a tag + si_tag=$si_previous_tag + si_previous_tag=$( hg --cwd "$si_repo_dir" log -r "last(parents($_si_tip))" --template '{latesttag}' 2>/dev/null ) + fi + si_previous_revision=$( hg --cwd "$si_repo_dir" log -r "$si_previous_tag" --template '{rev}' 2>/dev/null ) +} + +set_info_file() { + local _si_file="$1" + if [[ $si_repo_type == "git" ]]; then + local _si_file_dir=${_si_file%/*} # set the working directory to file's directory to allow for submodule file info + _si_file=${_si_file##*/} # just need the filename + # Populate filter vars from the last commit the file was included in. + si_file_author=$( git -C "$_si_file_dir" log --max-count=1 --format="%an" -- "$_si_file" 2>/dev/null ) + si_file_timestamp=$( git -C "$_si_file_dir" log --max-count=1 --format="%at" -- "$_si_file" 2>/dev/null ) + si_file_hash=$( git -C "$_si_file_dir" log --max-count=1 --format="%H" -- "$_si_file" 2>/dev/null ) + si_file_revision=$( git -C "$_si_file_dir" rev-list --count "$si_file_hash" 2>/dev/null ) # XXX checkout depth affects rev-list, see set_info_git + si_file_abbreviated_hash=$( git -C "$_si_file_dir" log --max-count=1 --abbrev=7 --format="%h" -- "$_si_file" 2>/dev/null ) + + elif [[ $si_repo_type == "svn" ]]; then + local _sif_svninfo _si_timestamp + _sif_svninfo=$( svn info "$_si_file" 2>/dev/null ) + if [[ -n $_sif_svninfo ]]; then + # Populate filter vars. + si_file_author=$( awk '/^Last Changed Author:/ { print $0; exit }' <<< "$_sif_svninfo" | cut -d" " -f4- ) + _si_timestamp=$( awk '/^Last Changed Date:/ { print $4,$5,$6; exit }' <<< "$_sif_svninfo" ) + si_file_timestamp=$( strtotime "$_si_timestamp" "%F %T %z" ) + si_file_revision=$( awk '/^Last Changed Rev:/ { print $NF; exit }' <<< "$_sif_svninfo" ) + # Use the file checksum. + si_file_hash=$( awk '/^Checksum:/ { print $2; exit }' <<< "$_sif_svninfo" ) + si_file_abbreviated_hash=${si_file_hash:0:7} + else + si_file_author= + si_file_timestamp= + si_file_revision= + si_file_hash= + si_file_abbreviated_hash= + si_file_date_iso= + si_file_date_integer= + fi + + elif [[ $si_repo_type == "hg" ]]; then + # Populate filter vars. + si_file_author=$( hg log --limit 1 --template '{author}' "$_si_file" 2>/dev/null ) + si_file_timestamp=$( hg log --limit 1 --template '{date}' "$_si_file" 2>/dev/null | cut -d. -f1 ) + si_file_revision=$( hg log --limit 1 --template '{rev}' "$_si_file" 2>/dev/null ) + si_file_hash=$( hg log --limit 1 --template '{node}' "$_si_file" 2>/dev/null ) + si_file_abbreviated_hash=$( hg log --limit 1 --template '{node|short}' "$_si_file" 2>/dev/null ) + + fi + if [[ -n $si_file_timestamp ]]; then + si_file_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_file_timestamp" ) + si_file_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_file_timestamp" ) + fi +} + +# Set some version info about the project +echo -ne "Reading project repository..." +case $repository_type in + git) set_info_git "$topdir" ;; + svn) set_info_svn "$topdir" ;; + hg) set_info_hg "$topdir" ;; +esac +echo -e "done\n" + +tag=$si_tag +project_version=$si_project_version +previous_version=$si_previous_tag +project_hash=$si_project_hash +project_revision=$si_project_revision +previous_revision=$si_previous_revision +project_timestamp=$si_project_timestamp +project_github_url= +project_github_slug= +if [[ "$si_repo_url" == "https://github.com"* ]]; then + project_github_url=${si_repo_url%.git} + project_github_slug=${project_github_url#https://github.com/} +fi +project_site= + +# Automatic file type detection based on CurseForge rules +# 1) Untagged commits will be marked as an alpha. +# 2) Tagged commits will be marked as a release with the following exceptions: +# - If the tag contains the word "alpha", it will be marked as an alpha file. +# - If instead the tag contains the word "beta", it will be marked as a beta file. +if [ -n "$tag" ]; then + if [[ "${tag,,}" == *"alpha"* ]]; then + file_type="alpha" + elif [[ "${tag,,}" == *"beta"* ]]; then + file_type="beta" + else + file_type="release" + fi +else + file_type="alpha" +fi + +# Add some GitHub Actions outputs +[[ -n $GITHUB_ACTIONS ]] && { + echo "project_version=${project_version}" + echo "previous_version=${previous_version}" + echo "project_hash=${project_hash}" + echo "project_timestamp=${project_timestamp}" + echo "release_type=${file_type}" +} >> "$GITHUB_OUTPUT" + +# Bare carriage-return character. +carriage_return=$( printf "\r" ) + +# Returns 0 if $1 matches one of the colon-separated patterns in $2. +match_pattern() { + local _mp_file="$1" + local _mp_list="$2:" + local _mp_pattern + while [ -n "$_mp_list" ]; do + _mp_pattern=${_mp_list%%:*} + _mp_list=${_mp_list#*:} + # shellcheck disable=SC2254 # _mp_pattern is a glob + case $_mp_file in + $_mp_pattern) + return 0 + ;; + esac + done + return 1 +} + + +# Simple .pkgmeta YAML processor. +declare -A yaml_bool=( ["yes"]="yes" ["true"]="yes" ["on"]="yes" ["false"]="no" ["off"]="no" ["no"]="no" ) +yaml_keyvalue() { + yaml_key=${1%%:*} + yaml_value=${1#"$yaml_key":} + yaml_value=${yaml_value#"${yaml_value%%[! ]*}"} # trim leading whitespace + if [[ -n "$yaml_value" && -n "${yaml_bool[${yaml_value,,}]}" ]]; then # normalize booleans + yaml_value="${yaml_bool[${yaml_value,,}]}" + fi + yaml_value=${yaml_value#[\'\"]} # trim leading quotes + yaml_value=${yaml_value%[\'\"]} # trim trailing quotes +} + +yaml_listitem() { + yaml_item=${1#-} + yaml_item=${yaml_item#"${yaml_item%%[! ]*}"} # trim leading whitespace + yaml_item=${yaml_item#[\'\"]} # trim leading quotes + yaml_item=${yaml_item%[\'\"]} # trim trailing quotes +} + +### +### Process .pkgmeta to set variables used later in the script. +### + +if [ -z "$pkgmeta_file" ]; then + pkgmeta_file="$topdir/.pkgmeta" + # CurseForge allows this so check for it + if [ ! -f "$pkgmeta_file" ] && [ -f "$topdir/pkgmeta.yaml" ]; then + pkgmeta_file="$topdir/pkgmeta.yaml" + fi +fi + +# Variables set via .pkgmeta. +package= +project= +manual_changelog= +changelog= +changelog_markup="text" +enable_nolib_creation= +ignore= +license= +unchanged= +zip_root_dirs=() +nolib_exclude= +wowi_gen_changelog="true" +wowi_archive="true" +wowi_convert_changelog="true" +declare -A relations=() + +parse_ignore() { + pkgmeta="$1" # full path to .pkgmeta + copy_path="$2" # the post-copy base path to match + sub_path="$3" # only includes matches in this path + + [ -f "$pkgmeta" ] || return 1 + + check_path="$topdir/" # paths are relative to the topdir + if [ -n "$copy_path" ]; then + check_path=$( dirname "$pkgmeta" )"/" + copy_path="$copy_path/" + fi + if [[ -n $sub_path && $sub_path != *"/" ]]; then + sub_path="$sub_path/" + fi + + yaml_eof= + while [ -z "$yaml_eof" ]; do + IFS='' read -r yaml_line || yaml_eof="true" + # Skip commented out lines. + if [[ $yaml_line =~ ^[[:space:]]*\# ]]; then + continue + fi + # Strip any trailing CR character. + yaml_line="${yaml_line%"$carriage_return"}" + + case $yaml_line in + [!\ ]*:*) + # Split $yaml_line into a $yaml_key, $yaml_value pair. + yaml_keyvalue "$yaml_line" + # Set the $pkgmeta_phase for stateful processing. + pkgmeta_phase="$yaml_key" + ;; + [\ ]*"- "*) + yaml_line="${yaml_line#"${yaml_line%%[! ]*}"}" # trim leading whitespace + # Get the YAML list item. + yaml_listitem "$yaml_line" + if [[ "$pkgmeta_phase" == "ignore" || "$pkgmeta_phase" == "plain-copy" ]] && [[ $yaml_item == "$sub_path"* ]]; then + yaml_item=${yaml_item%"/*"} # trim dir glob + pattern=${yaml_item#$sub_path} # match relative to sub_path + if [ -d "$check_path$yaml_item" ]; then + pattern="$copy_path$pattern/*" + elif [ ! -f "$check_path$yaml_item" ]; then + # doesn't exist so match both a file and a path + pattern="$copy_path$pattern:$copy_path$pattern/*" + else + pattern="$copy_path$pattern" + fi + if [[ "$pkgmeta_phase" == "ignore" ]]; then + if [ -z "$ignore" ]; then + ignore="$pattern" + else + ignore="$ignore:$pattern" + fi + elif [[ "$pkgmeta_phase" == "plain-copy" ]]; then + if [ -z "$unchanged" ]; then + unchanged="$pattern" + else + unchanged="$unchanged:$pattern" + fi + fi + fi + ;; + esac + done < "$pkgmeta" +} + +if [ -f "$pkgmeta_file" ]; then + if grep -q $'^[ ]*\t\+[[:blank:]]*[[:graph:]]' "$pkgmeta_file"; then + # Try to cut down on some troubleshooting pain. + echo "ERROR! Your pkgmeta file contains a leading tab. Only spaces are allowed for indentation in YAML files." >&2 + grep -n $'^[ ]*\t\+[[:blank:]]*[[:graph:]]' "$pkgmeta_file" | sed $'s/\t/^I/g' + exit 1 + fi + + yaml_eof= + while [ -z "$yaml_eof" ]; do + IFS='' read -r yaml_line || yaml_eof="true" + # Skip commented out lines. + if [[ $yaml_line =~ ^[[:space:]]*\# ]]; then + continue + fi + # Strip any trailing CR character. + yaml_line="${yaml_line%"$carriage_return"}" + + case $yaml_line in + [!\ ]*:*) + # Split $yaml_line into a $yaml_key, $yaml_value pair. + yaml_keyvalue "$yaml_line" + # Set the $pkgmeta_phase for stateful processing. + pkgmeta_phase="$yaml_key" + + case $yaml_key in + enable-nolib-creation) + if [ "$yaml_value" = "yes" ]; then + enable_nolib_creation="true" + fi + ;; + enable-toc-creation) + if [ "$yaml_value" = "yes" ]; then + split="true" + fi + ;; + license-output) + license=$yaml_value + ;; + manual-changelog) + changelog="$yaml_value" + manual_changelog="true" + ;; + changelog-title) + project="$yaml_value" + ;; + package-as) + package="$yaml_value" + ;; + wowi-create-changelog) + if [ "$yaml_value" = "no" ]; then + wowi_gen_changelog= + fi + ;; + wowi-convert-changelog) + if [ "$yaml_value" = "no" ]; then + wowi_convert_changelog= + fi + ;; + wowi-archive-previous) + if [ "$yaml_value" = "no" ]; then + wowi_archive= + fi + ;; + esac + ;; + " "*) + yaml_line=${yaml_line#"${yaml_line%%[! ]*}"} # trim leading whitespace + case $yaml_line in + "- "*) + # Get the YAML list item. + yaml_listitem "$yaml_line" + case $pkgmeta_phase in + tools-used) + relations["${yaml_item,,}"]="tool" + ;; + required-dependencies) + relations["${yaml_item,,}"]="requiredDependency" + ;; + optional-dependencies) + relations["${yaml_item,,}"]="optionalDependency" + ;; + embedded-libraries) + relations["${yaml_item,,}"]="embeddedLibrary" + ;; + esac + ;; + *:*) + # Split $yaml_line into a $yaml_key, $yaml_value pair. + yaml_keyvalue "$yaml_line" + case $pkgmeta_phase in + manual-changelog) + case $yaml_key in + filename) + changelog="$yaml_value" + manual_changelog="true" + ;; + markup-type) + if [ "$yaml_value" = "markdown" ] || [ "$yaml_value" = "html" ]; then + changelog_markup="$yaml_value" + else + changelog_markup="text" + fi + ;; + esac + ;; + move-folders) + # Save project root directories + if [[ $yaml_value != *"/"* ]]; then + _mf_path="${yaml_key#*/}" # strip the package name + toc_root_paths["$topdir/$_mf_path"]="$yaml_value" + fi + ;; + esac + ;; + esac + ;; + esac + done < "$pkgmeta_file" +fi + +# Add untracked/ignored files to the ignore list + +vcs_addignore() { + local _ignored_path="$1" + if [[ -d "$topdir/$_ignored_path" ]]; then + _ignored_path="$_ignored_path*" + fi + # Don't ignore a manual changelog generated for the build + if [[ -z $changelog || $_ignored_path != "$changelog" ]]; then + if [[ -z "$ignore" ]]; then + ignore="$_ignored_path" + else + ignore="$ignore:$_ignored_path" + fi + fi +} + +OLDIFS=$IFS +IFS=$'\n' +if [ "$repository_type" = "git" ]; then + for _vcs_ignore in $( git -C "$topdir" ls-files --others --directory ); do + vcs_addignore "$_vcs_ignore" + done +elif [ "$repository_type" = "svn" ]; then + for _vcs_ignore in $( cd "$topdir" && svn status --no-ignore --ignore-externals | awk '/^[?IX]/ { print $2 }' ); do + vcs_addignore "${_vcs_ignore//\\//}" + done +elif [ "$repository_type" = "hg" ]; then + for _vcs_ignore in $( hg --cwd "$topdir" status --ignored --unknown --no-status ); do + vcs_addignore "$_vcs_ignore" + done +fi +IFS=$OLDIFS + + +# Add .pkgmeta ignored files to the ignore list +parse_ignore "$pkgmeta_file" + +### +### Process TOC file +### + +set_info_toc_interface() { + local toc_path="$1" + local package_name="$2" + + si_game_type_interface=() + si_game_type_interface_all=() + si_game_root_interface= + [[ -z $package_name ]] && return 0 + + local toc_name=${toc_path##*/} + + local toc_suffix toc_file_game_type + if [[ $toc_name =~ "$package_name"[-_](Mainline|Classic|Vanilla|BCC|TBC|Wrath|WOTLKC|Cata|Mists|Camelot)\.toc$ ]]; then + toc_suffix="${BASH_REMATCH[1],,}" + toc_file_game_type="${game_flavor[$toc_suffix]}" + fi + + local toc_file toc_version toc_game_type + toc_file=$( + # Remove BOM and CR and apply some non-version related TOC filters + [[ $file_type != "alpha" ]] && _tf_alpha="true" + sed -e $'1s/^\xEF\xBB\xBF//' -e $'s/\r//g' "$toc_path" | toc_filter alpha ${_tf_alpha:+true} | toc_filter debug true + ) + + toc_version=$( awk -F: '/^## Interface:/ { gsub(/[ \t]/, "", $2); print $2; exit }' <<< "$toc_file" ) + if [[ -n $toc_version ]]; then + local toc_version_game_type + IFS=',' read -ra V <<< "$toc_version" + toc_to_file_type "${V[0]}" "toc_game_type" + for i in "${V[@]}"; do + toc_to_type "$i" "toc_version_game_type" + if [[ -n ${si_game_type_interface_all[$toc_version_game_type]} ]]; then + si_game_type_interface_all[$toc_version_game_type]="${si_game_type_interface_all[$toc_version_game_type]}:$i" + else + si_game_type_interface_all[$toc_version_game_type]="$i" + fi + + # Are the interface values all for one game type? (Using the base game type) + if [[ -n $toc_game_type ]]; then + toc_to_file_type "$i" "toc_version_game_type" + if [[ $toc_game_type != "$toc_version_game_type" ]]; then + toc_game_type= + fi + fi + done + + # Splitting needs the fallback toc to be game typed, so make sure we're not mixing strategies + # if [[ -n $split && -z $toc_game_type ]]; then + # echo "$toc_name has multiple game type interface versions as the fallback ($toc_version) but creating TOC files is enabled." >&2 + # echo "Add additional \"## Interface-[Type]:\" lines for each game type TOC file to create." >&2 + # exit 1 + # fi + + toc_version=$( IFS=':' ; echo "${V[*]}" ) # swap delimiter + si_game_root_interface="$toc_version" + fi + if [[ -n $toc_suffix ]]; then + # Flavored, just validate the version + if [[ -z $toc_version ]]; then + echo "$toc_name is missing an interface version." >&2 + exit 1 + fi + if [[ $toc_suffix == "classic" ]]; then + # Special check for _Classic (the classic game family) to allow any classic version + IFS=':' read -ra V <<< "$toc_version" + for i in "${V[@]}"; do + toc_to_type "$i" "toc_file_game_type" + if [[ $toc_file_game_type == "retail" || $toc_file_game_type == "forever" ]]; then + echo "$toc_name has an interface version ($i) that is not compatible with the game type \"Classic\"." >&2 + exit 1 + fi + done + elif [[ $toc_suffix == "mainline" ]]; then + # Special check for _Mainline (the "modern" game family) to allow retail (standard) and forever versions + IFS=':' read -ra V <<< "$toc_version" + for i in "${V[@]}"; do + toc_to_type "$i" "toc_file_game_type" + if [[ $toc_file_game_type != "retail" && $toc_file_game_type != "forever" ]]; then + echo "$toc_name has an interface version ($i) that is not compatible with the game type \"Mainline\"." >&2 + exit 1 + fi + done + elif [[ $toc_file_game_type != "$toc_game_type" ]]; then + # Other suffixes are required to match the game type + echo "$toc_name has an interface version (${toc_version//:/,}) that is not compatible with the game version \"${toc_file_game_type}\"." >&2 + exit 1 + fi + else + # Fallback + local game_type_toc_version toc_file_game_type + + # Save the game type interface values + for type in "${!game_flavor[@]}"; do + game_type_toc_version=$( awk -F: 'tolower($0) ~ /^## interface-'"$type"':/ { gsub(/[ \t]/, "", $2); print $2; exit }' <<< "$toc_file" ) + if [[ -n $game_type_toc_version ]]; then + type="${game_flavor[$type]}" + IFS=',' read -ra V <<< "$game_type_toc_version" + for i in "${V[@]}"; do + toc_to_type "$i" "toc_file_game_type" + if [[ $toc_file_game_type != "$type" ]]; then + echo "$toc_name has an interface version ($i) that is not compatible with \"Interface-${type^}\"." >&2 + exit 1 + fi + done + game_type_toc_version=$( IFS=':' ; echo "${V[*]}" ) # swap delimiter + si_game_type_interface[$type]="$game_type_toc_version" + si_game_type_interface_all[$type]="$game_type_toc_version" + fi + done + + # Use the game type if set, otherwise default to retail + game_type_toc_version="${si_game_type_interface_all[${game_type:-retail}]}" + # No "Interface:" value or game type doesn't match, try to use the "Interface-Type:" value + if [[ -z $toc_version ]] || [[ -n $game_type && -n $game_type_toc_version && $game_type_toc_version != "$toc_version" ]]; then + toc_version="$game_type_toc_version" + toc_game_type="${game_type:-retail}" + fi + + # Check @non-@ blocks for an interface matching the game type (DEPRECATED, remove this in v3) + # XXX toc_game_type is the base game type, so we check si_game_type_interface_all for the manually set game type (band-aid for titan⊃wrath) + if [[ -z $toc_version ]] || [[ -n $game_type && $toc_game_type != "$game_type" && -z ${si_game_type_interface_all[$game_type]} ]]; then + toc_game_type="$game_type" + local game_type_toc_prefix + case $toc_game_type in + classic) game_type_toc_prefix="11[345]" ;; + bcc) game_type_toc_prefix="20" ;; + wrath) game_type_toc_prefix="30" ;; + cata) game_type_toc_prefix="40" ;; + mists) game_type_toc_prefix="50" ;; + titan) game_type_toc_prefix="380" ;; + forever) game_type_toc_prefix="16[0-9]" ;; + *) game_type_toc_prefix= + esac + if [[ -n $game_type_toc_prefix ]]; then + toc_version=$( sed -n '/@non-[-a-z]*@/,/@end-non-[-a-z]*@/{//b;p}' <<< "$toc_file" | awk -F: '/#[ \t]*## Interface:[ \t]*('"$game_type_toc_prefix"')/ { gsub(/[ \t]/, "", $2); print $2; exit }' ) + IFS=',' read -ra V <<< "$toc_version" + for i in "${V[@]}"; do + toc_to_type "$i" "toc_file_game_type" + if [[ $toc_file_game_type != "$toc_game_type" ]]; then + echo "$toc_name has an interface version ($i) that is not compatible with the game version \"${game_flavor[$toc_game_type]}\"." >&2 + exit 1 + fi + done + toc_version=$( IFS=':' ; echo "${V[*]}" ) # swap delimiter + + # This becomes the actual interface version after replacements + si_game_root_interface="$toc_version" + si_game_type_interface_all[$toc_game_type]="$toc_version" + fi + fi + + # Only game type TOC files will be created, so a fallback interface version isn't needed + if [[ -z $toc_version && -n $split && -z $game_type && ${#si_game_type_interface[@]} -gt 0 ]]; then + return 0 + fi + + # End of the line + if [[ -z $toc_version ]]; then + if [[ -z $toc_game_type ]]; then + echo "$toc_name is missing an interface version." >&2 + else + echo "$toc_name does not have an interface version that is compatible with the game version \"$toc_game_type\"." >&2 + fi + exit 1 + fi + fi +} + +set_toc_project_info() { + local toc_path="$1" + # Get the title of the addon for the changelog + if [ -z "$project" ]; then + project=$( sed -e $'1s/^\xEF\xBB\xBF//' -e $'s/\r//g' "$toc_path" | awk '/^## Title:/ { print $0; exit }' | sed -e 's/|c[0-9A-Fa-f]\{8\}//g' -e 's/|r//g' -e 's/|T[^|]*|t//g' -e 's/## Title[[:space:]]*:[[:space:]]*\(.*\)/\1/' -e 's/[[:space:]]*$//' ) + fi + # Get project IDs for uploading + if [ -z "$slug" ]; then + slug=$( sed -e $'1s/^\xEF\xBB\xBF//' -e $'s/\r//g' "$toc_path" | awk -F: '/^## X-Curse-Project-ID:/ { gsub(/[ \t]/, "", $2); print $2; exit }' ) + fi + if [ -z "$addonid" ]; then + addonid=$( sed -e $'1s/^\xEF\xBB\xBF//' -e $'s/\r//g' "$toc_path" | awk -F: '/^## X-WoWI-ID:/ { gsub(/[ \t]/, "", $2); print $2; exit }' ) + fi + if [ -z "$wagoid" ]; then + wagoid=$( sed -e $'1s/^\xEF\xBB\xBF//' -e $'s/\r//g' "$toc_path" | awk -F: '/^## X-Wago-ID:/ { gsub(/[ \t]/, "", $2); print $2; exit }' ) + fi +} + +set_build_version() { + if [[ -z $game_version ]]; then + local toc_version toc_game_type path_interface path_version + for path in "${toc_paths[@]}"; do + if [[ -z "$split" && -z "$game_type" ]]; then + # no split and no game type means we should use the root interface value + toc_version="${toc_root_interface[$path]}" + else + toc_version="${toc_interfaces[$path]}" + fi + IFS=':' read -ra V <<< "$toc_version" + for i in "${V[@]}"; do + toc_to_type "$i" "toc_game_type" + # add the version for every root addon + if [[ -z $game_type || $game_type == "$toc_game_type" ]]; then + path_interface="$i" + path_version=$( printf "%d.%d.%d" $(( 10#${i:0: -4} )) $(( 10#${i: -4:2} )) $(( 10#${i: -2:2} )) ) + if [[ -n ${game_type_interface[$toc_game_type]} ]]; then + if [[ ":${game_type_interface[$toc_game_type]}:" != *":$path_interface:"* ]]; then + game_type_interface[$toc_game_type]="${game_type_interface[$toc_game_type]}:$path_interface" + game_type_version[$toc_game_type]="${game_type_version[$toc_game_type]}:$path_version" + fi + else + game_type_interface[$toc_game_type]="$path_interface" + game_type_version[$toc_game_type]="$path_version" + fi + fi + done + done + fi + + # Set the game versions shown in the summary + game_version=$( + if [[ -n $game_type ]]; then + readarray -t sorted < <( printf '%s\n' "${game_type_version[$game_type]//:/$'\n'}" | sort -Vr ) + else + readarray -t sorted < <( printf '%s\n' "${game_type_version[@]//:/$'\n'}" | sort -Vr ) + fi + IFS=',' ; echo "${sorted[*]}" + ) + + # Set the game type when we only have one game version + if [[ -z $game_type && ${#game_type_version[@]} -eq 1 ]]; then + game_type="${!game_type_version[*]}" + fi +} + +# Set the package name from a TOC file name +if [[ -z "$package" ]]; then + # grab the shortest toc file in the top directory + package=$( cd "$topdir" && find ./*.toc -maxdepth 0 2>/dev/null | awk '{ print length(), substr($0, 3) }' | sort -n | cut -d" " -f2- | head -n1 ) + if [[ -z "$package" ]]; then + echo "Could not find an addon TOC file. In another directory? Set 'package-as' in .pkgmeta" >&2 + exit 1 + fi + package=${package%.toc} + if [[ $package =~ ^(.*)([-_](Mainline|Classic|Vanilla|BCC|TBC|Wrath|WOTLKC|Cata|Mists|Camelot))$ ]]; then + echo "Ambiguous addon name. No fallback TOC file or addon name includes an expansion suffix (${BASH_REMATCH[2]}). Set 'package-as' in .pkgmeta" >&2 + exit 1 + fi +fi + +# Parse the project root TOC files for info first +for toc_path in "$topdir/$package"{,-Mainline,_Mainline,-Classic,_Classic,-Vanilla,_Vanilla,-BCC,_BCC,-TBC,_TBC,-Wrath,_Wrath,-WOTLKC,_WOTLKC,-Cata,_Cata,-Mists,_Mists,-Camelot,_Camelot}.toc; do + if [[ -f "$toc_path" ]]; then + set_toc_project_info "$toc_path" + toc_paths+=("$toc_path") + toc_root_paths["$topdir"]="$package" + fi +done +# Try parsing the project addon in move-folders for info next +for path in "${!toc_root_paths[@]}"; do + if [[ ${toc_root_paths[$path]} == "$package" && $path != "$topdir" ]]; then + for toc_path in "$path/$package"{,-Mainline,_Mainline,-Classic,_Classic,-Vanilla,_Vanilla,-BCC,_BCC,-TBC,_TBC,-Wrath,_Wrath,-WOTLKC,_WOTLKC,-Cata,_Cata,-Mists,_Mists,-Camelot,_Camelot}.toc; do + if [[ -f "$toc_path" ]]; then + set_toc_project_info "$toc_path" + fi + done + fi +done + +# Parse project TOC files +for path in "${!toc_root_paths[@]}"; do + for toc_path in "$path/${toc_root_paths[$path]}"{,-Mainline,_Mainline,-Classic,_Classic,-Vanilla,_Vanilla,-BCC,_BCC,-TBC,_TBC,-Wrath,_Wrath,-WOTLKC,_WOTLKC,-Cata,_Cata,-Mists,_Mists,-Camelot,_Camelot}.toc; do + if [[ -f "$toc_path" ]]; then + set_toc_project_info "$toc_path" + set_info_toc_interface "$toc_path" "${toc_root_paths[$path]}" + toc_root_interface[$toc_path]="$si_game_root_interface" + toc_interfaces[$toc_path]=$( IFS=':' ; echo "${si_game_type_interface_all[*]}" ) + if [[ " ${toc_paths[*]} " != *" $toc_path "* ]]; then + toc_paths+=("$toc_path") + fi + fi + done +done + +if [[ ${#toc_interfaces[@]} -eq 0 ]]; then + echo "Could not find an addon TOC file. In another directory? Make sure it matches the 'package-as' in .pkgmeta" >&2 + exit 1 +fi + +if [[ -n "$split" ]]; then + if [[ "${toc_interfaces[*]}" != *":"* ]]; then + echo "Creating TOC files is enabled but there is only one TOC interface version per file?" >&2 + for path in "${toc_paths[@]}"; do + [[ -n ${toc_interfaces[$path]} ]] && echo " ${path##$topdir/} [${toc_interfaces[$path]}]" >&2 + done + exit 1 + fi +fi + +set_build_version + +if [[ -z "$game_version" ]]; then + echo "There was a problem setting the build version. Do you have a base \"# Interface:\" line in your TOC files?" >&2 + exit 1 +fi + +# Unset project ids if they are set to 0 +[ "$slug" = "0" ] && slug= +[ "$addonid" = "0" ] && addonid= +[ "$wagoid" = "0" ] && wagoid= + +echo "Packaging $package" +if [ -n "$project_version" ]; then + echo "Current version: $project_version" +fi +if [ -n "$previous_version" ]; then + echo "Previous version: $previous_version" +fi +( + if [[ -n $game_type ]]; then + [[ $game_type == "retail" ]] && version="retail " || version="non-retail version-${game_type} " + elif [[ ${#game_type_version[@]} -gt 1 ]]; then + version="multi-version " + fi + [ "$file_type" = "alpha" ] && alpha="alpha" || alpha="non-alpha" + echo "Build type: ${version}${alpha} non-debug${nolib:+ nolib}" + echo "Game version: ${game_version//,/, }" + echo +) +if [[ "$slug" =~ ^[0-9]+$ ]]; then + project_site="https://wow.curseforge.com" + echo "CurseForge ID: $slug${cf_token:+ [token set]}" +fi +if [ -n "$addonid" ]; then + echo "WoWInterface ID: $addonid${wowi_token:+ [token set]}" +fi +if [ -n "$wagoid" ]; then + echo "Wago ID: $wagoid${wago_token:+ [token set]}" +fi +if [ -n "$project_github_slug" ]; then + echo "GitHub: $project_github_slug${github_token:+ [token set]}" +fi +if [ -n "$project_site" ] || [ -n "$addonid" ] || [ -n "$wagoid" ] || [ -n "$project_github_slug" ]; then + echo +fi +echo "Checkout directory: $topdir" +echo "Release directory: $releasedir" +echo + +# Set $pkgdir to the path of the package directory inside $releasedir. +pkgdir="$releasedir/$package" +if [ -d "$pkgdir" ] && [ -z "$overwrite" ]; then + #echo "Removing previous package directory: $pkgdir" + rm -fr "$pkgdir" +fi +if [ ! -d "$pkgdir" ]; then + mkdir -p "$pkgdir" +fi + +# Set the contents of the addon zipfile. +zip_root_dirs+=("$package") + +### +### Create filters for pass-through processing of files to replace repository keywords. +### + +# Filter for simple repository keyword replacement. +vcs_filter() { + sed \ + -e "s/@project-revision@/$si_project_revision/g" \ + -e "s/@project-hash@/$si_project_hash/g" \ + -e "s/@project-abbreviated-hash@/$si_project_abbreviated_hash/g" \ + -e "s/@project-author@/$( escape_substr "$si_project_author" )/g" \ + -e "s/@project-date-iso@/$si_project_date_iso/g" \ + -e "s/@project-date-integer@/$si_project_date_integer/g" \ + -e "s/@project-timestamp@/$si_project_timestamp/g" \ + -e "s/@project-version@/$( escape_substr "$si_project_version" )/g" \ + -e "s/@file-revision@/$si_file_revision/g" \ + -e "s/@file-hash@/$si_file_hash/g" \ + -e "s/@file-abbreviated-hash@/$si_file_abbreviated_hash/g" \ + -e "s/@file-author@/$( escape_substr "$si_file_author" )/g" \ + -e "s/@file-date-iso@/$si_file_date_iso/g" \ + -e "s/@file-date-integer@/$si_file_date_integer/g" \ + -e "s/@file-timestamp@/$si_file_timestamp/g" \ + -e "s/@build-date@/$si_build_date/g" \ + -e "s/@build-date-iso@/$si_build_date_iso/g" \ + -e "s/@build-date-integer@/$si_build_date_integer/g" \ + -e "s/@build-timestamp@/$si_build_timestamp/g" +} + +# Find URL of localization api. +set_localization_url() { + localization_url= + if [ -n "$slug" ] && [ -n "$cf_token" ] && [ -n "$project_site" ]; then + localization_url="${project_site}/api/projects/$slug/localization/export" + fi + if [ -z "$localization_url" ] && find "$topdir" -path '*/.*' -prune -o -name "*.lua" -print0 | xargs -0 grep -q "@localization"; then + echo "Skipping localization! Missing CurseForge API token and/or project id is invalid." + echo + fi +} + +# Filter to handle @localization@ repository keyword replacement. +# https://support.curseforge.com/support/solutions/articles/9000197354-localization-substitutions +declare -A unlocalized_values=( ["english"]="ShowPrimary" ["comment"]="ShowPrimaryAsComment" ["blank"]="ShowBlankAsComment" ["ignore"]="Ignore" ) +localization_filter() { + _ul_eof= + while [ -z "$_ul_eof" ]; do + IFS='' read -r _ul_line || _ul_eof="true" + # Strip any trailing CR character. + _ul_line=${_ul_line%$carriage_return} + case $_ul_line in + *@localization\(*\)@*) + _ul_lang= + _ul_namespace= + _ul_singlekey= + _ul_tablename="L" + # Get the prefix of the line before the comment. + _ul_prefix=${_ul_line%%@localization(*} + _ul_prefix=${_ul_prefix%%--*} + # Strip everything but the localization parameters. + _ul_params=${_ul_line#*@localization(} + _ul_params=${_ul_params%)@} + # Sanitize the params a bit. (namespaces are restricted to [a-zA-Z0-9_], separated by [./:]) + _ul_params=${_ul_params// /} + _ul_params=${_ul_params//,/, } + # Pull the locale language first (mainly for warnings). + _ul_lang="enUS" + if [[ $_ul_params == *"locale=\""* ]]; then + _ul_lang=${_ul_params##*locale=\"} + _ul_lang=${_ul_lang:0:4} + _ul_lang=${_ul_lang%%\"*} + else + echo " Warning! No locale set, using enUS." >&3 + fi + # Generate a URL parameter string from the localization parameters. + # https://support.curseforge.com/support/solutions/articles/9000197321-curseforge-upload-api#Localization + _ul_url_params="" + # shellcheck disable=SC2086 # split on spaces to iterate key=value pairs + set -- ${_ul_params} + for _ul_param; do + _ul_key=${_ul_param%%=*} + _ul_value=${_ul_param#*=} + _ul_value=${_ul_value%,*} + _ul_value=${_ul_value#*\"} + _ul_value=${_ul_value%\"*} + case ${_ul_key} in + escape-non-ascii) + if [ "$_ul_value" = "true" ]; then + _ul_url_params="${_ul_url_params}&escape-non-ascii-characters=true" + fi + ;; + format) + if [ "$_ul_value" = "lua_table" ]; then + _ul_url_params="${_ul_url_params}&export-type=Table" + fi + ;; + handle-unlocalized) + if [ "$_ul_value" != "english" ] && [ -n "${unlocalized_values[$_ul_value]}" ]; then + _ul_url_params="${_ul_url_params}&unlocalized=${unlocalized_values[$_ul_value]}" + fi + ;; + handle-subnamespaces) + if [ "$_ul_value" = "concat" ]; then # concat with / + _ul_url_params="${_ul_url_params}&concatenante-subnamespaces=true" + elif [ "$_ul_value" = "subtable" ]; then + echo " ($_ul_lang) Warning! ${_ul_key}=\"${_ul_value}\" is not supported. Include each full subnamespace, comma delimited." >&3 + fi + ;; + key) + # _ul_params was stripped of spaces, so reparse the line for the key + _ul_singlekey=${_ul_line#*@localization(} + _ul_singlekey=${_ul_singlekey#*key=\"} + _ul_singlekey=${_ul_singlekey%%\",*} + _ul_singlekey=${_ul_singlekey%%\")@*} + ;; + locale) + _ul_lang=$_ul_value + ;; + namespace) + # reparse to get all namespaces if multiple + _ul_namespace=${_ul_params##*namespace=\"} + _ul_namespace=${_ul_namespace%%\"*} + _ul_namespace=${_ul_namespace//, /,} + _ul_url_params="${_ul_url_params}&namespaces=${_ul_namespace}" + _ul_namespace="/${_ul_namespace}" + ;; + namespace-delimiter) + if [ "$_ul_value" != "/" ]; then + echo " ($_ul_lang) Warning! ${_ul_key}=\"${_ul_value}\" is not supported." >&3 + fi + ;; + prefix-values) + echo " ($_ul_lang) Warning! \"${_ul_key}\" is not supported." >&3 + ;; + same-key-is-true) + if [ "$_ul_value" = "true" ]; then + _ul_url_params="${_ul_url_params}&true-if-value-equals-key=true" + fi + ;; + table-name) + if [ "$_ul_value" != "L" ]; then + _ul_tablename="$_ul_value" + _ul_url_params="${_ul_url_params}&table-name=${_ul_value}" + fi + ;; + esac + done + + if [ -z "$_cdt_localization" ] || [ -z "$localization_url" ]; then + echo " Skipping localization (${_ul_lang}${_ul_namespace})" >&3 + + # If the line isn't a TOC entry, print anything before the keyword. + if [[ $_ul_line != "## "* ]]; then + if [ -n "$_ul_eof" ]; then + echo -n "$_ul_prefix" + else + echo "$_ul_prefix" + fi + fi + else + _ul_url="${localization_url}?lang=${_ul_lang}${_ul_url_params}" + echo " Adding ${_ul_lang}${_ul_namespace}" >&3 + + if [ -z "$_ul_singlekey" ]; then + # Write text that preceded the substitution. + echo -n "$_ul_prefix" + + # Fetch the localization data, but don't output anything if there is an error. + curl -sL -H "x-api-token: $cf_token" "${_ul_url}" | awk -v url="$_ul_url" '/^{"error/ { o=" \033[01;31mError! "$0"\033[0m\n "url; print o >"/dev/fd/3"; exit 1 } /"/dev/fd/3"; exit 1 } /^<(HTML|html)>/ { print " \033[01;31mError! Invalid output\033[0m\n "url >"/dev/fd/3"; exit 1 } /^'"$_ul_tablename"' = '"$_ul_tablename"' or \{\}/ { next } { print }' || exit 1 + + # Insert a trailing blank line to match CF packager. + if [ -z "$_ul_eof" ]; then + echo "" + fi + else + # Parse out a single phrase. This is kind of expensive, but caching would be way too much effort to optimize for what is basically an edge case. + _ul_value=$( curl -sL -H "x-api-token: $cf_token" "${_ul_url}" | awk -v url="$_ul_url" '/^{"error/ { o=" \033[01;31mError! "$0"\033[0m\n "url; print o >"/dev/fd/3"; exit 1 } /"/dev/fd/3"; exit 1 } /^<(HTML|html)>/ { print " \033[01;31mError! Invalid output\033[0m\n "url >"/dev/fd/3"; exit 1 } { print }' | sed -n '/L\["'"$_ul_singlekey"'"\]/p' | sed 's/^.* = "\(.*\)"/\1/' ) + if [ -n "$_ul_value" ] && [ "$_ul_value" != "$_ul_singlekey" ]; then + # The result is different from the base value so print out the line. + echo "${_ul_prefix}${_ul_value}${_ul_line##*)@}" + fi + fi + fi + ;; + *) + if [ -n "$_ul_eof" ]; then + echo -n "$_ul_line" + else + echo "$_ul_line" + fi + esac + done +} + +lua_filter() { + local keyword="$1" + local width + case $keyword in + alpha) width="=" ;; + debug) width="==" ;; + retail|version-*) width="====" ;; + *) width="===" + esac + sed \ + -e "s/--@${keyword}@/--[${width}[@${keyword}@/g" \ + -e "s/--@end-${keyword}@/--@end-${keyword}@]${width}]/g" \ + -e "s/--\[===\[@non-${keyword}@/--@non-${keyword}@/g" \ + -e "s/--@end-non-${keyword}@\]===\]/--@end-non-${keyword}@/g" +} + +toc_interface_filter() { + local toc_version="${1//:/, }" + local current_toc_version="${2//:/, }" + # TOC version isn't what is set in the TOC file + if [[ -n "$toc_version" && "$current_toc_version" != "$toc_version" ]]; then + # Always remove BOM so ^ works + if [ -n "$current_toc_version" ]; then # rewrite + sed -e $'1s/^\xEF\xBB\xBF//' -e 's/^## Interface:.*$/## Interface: '"$toc_version"'/' -e '/^## Interface-/d' + else # add + sed -e $'1s/^\xEF\xBB\xBF//' -e '1i\ +## Interface: '"$toc_version" -e '/^## Interface-/d' + fi + if [[ -z "$split" ]]; then + echo " Set Interface to ${toc_version}" >&3 + fi + else # cleanup + sed -e $'1s/^\xEF\xBB\xBF//' -e '/^## Interface-/d' + fi +} + +xml_filter() { + sed \ + -e "s///@end-$1@-->/g" \ + -e "s//g" \ + -e "s/@end-non-$1@-->//g" +} + +do_not_package_filter() { + case $1 in + lua) sed '/--@do-not-package@/,/--@end-do-not-package@/d' ;; + toc) sed '/#@do-not-package@/,/#@end-do-not-package@/d' ;; + xml) sed '//,//d' ;; + esac +} + +line_ending_filter() { + local _lef_eof _lef_line + while [ -z "$_lef_eof" ]; do + IFS='' read -r _lef_line || _lef_eof="true" + # Strip any trailing CR character. + _lef_line=${_lef_line%$carriage_return} + if [ -n "$_lef_eof" ]; then + # Preserve EOF not preceded by newlines. + echo -n "$_lef_line" + else + case $line_ending in + dos) printf "%s\r\n" "$_lef_line" ;; # Terminate lines with CR LF. + unix) printf "%s\n" "$_lef_line" ;; # Terminate lines with LF. + esac + fi + done +} + +### +### Copy files from the working directory into the package directory. +### + +# Copy of the contents of the source directory into the destination directory. +# Dotfiles and any files matching the ignore pattern are skipped. Copied files +# are subject to repository keyword replacement. +# +copy_directory_tree() { + _cdt_alpha= + _cdt_debug= + _cdt_ignored_patterns= + _cdt_localization= + _cdt_nolib= + _cdt_do_not_package= + _cdt_unchanged_patterns= + _cdt_gametype= + _cdt_external= + _cdt_split= + OPTIND=1 + while getopts :adi:lnpu:g:eS _cdt_opt "$@"; do + # shellcheck disable=SC2220 # not user facing, don't need to handle invalid options + case $_cdt_opt in + a) _cdt_alpha="true" ;; + d) _cdt_debug="true" ;; + i) _cdt_ignored_patterns=$OPTARG ;; + l) _cdt_localization="true" + set_localization_url + ;; + n) _cdt_nolib="true" ;; + p) _cdt_do_not_package="true" ;; + u) _cdt_unchanged_patterns=$OPTARG ;; + g) _cdt_gametype=$OPTARG ;; + e) _cdt_external="true" ;; + S) _cdt_split="true" ;; + esac + done + shift $((OPTIND - 1)) + _cdt_srcdir=$1 + _cdt_destdir=$2 + + if [ -z "$_cdt_external" ]; then + start_group "Copying files into ${_cdt_destdir#$topdir/}:" "copy" + else # don't nest groups + echo "Copying files into ${_cdt_destdir#$topdir/}:" + fi + if [ ! -d "$_cdt_destdir" ]; then + mkdir -p "$_cdt_destdir" + fi + + # Create a "find" command to list all of the files in the current directory, minus any ones we need to prune. + _cdt_find_cmd="find ." + # Prune everything that begins with a dot except for the current directory ".". + _cdt_find_cmd+=" \( -name \".*\" -a \! -name \".\" \) -prune" + # Prune the destination directory if it is a subdirectory of the source directory. + _cdt_dest_subdir=${_cdt_destdir#${_cdt_srcdir}/} + case $_cdt_dest_subdir in + /*) ;; + *) _cdt_find_cmd+=" -o -path \"./$_cdt_dest_subdir\" -prune" ;; + esac + # Print the filename, but suppress the current directory ".". + _cdt_find_cmd+=" -o \! -name \".\" -print" + + local file + while read -r file; do # <( cd "$_cdt_srcdir" && eval "$_cdt_find_cmd" ) + file=${file#./} + _cdt_source_file="$_cdt_srcdir/$file" + if [ -f "$_cdt_source_file" ]; then + _cdt_skip_copy= + _cdt_only_copy= + # Prefix external files with the relative pkgdir path + _cdt_check_file=$file + if [ -n "${_cdt_destdir#$pkgdir}" ]; then + _cdt_check_file="${_cdt_destdir#$pkgdir/}/$file" + fi + # Skip files matching the colon-separated "ignored" shell wildcard patterns. + if match_pattern "$_cdt_check_file" "$_cdt_ignored_patterns"; then + _cdt_skip_copy="true" + fi + # Don't process files that match the colon-separated "unchanged" shell wildcard patterns. + if match_pattern "$_cdt_check_file" "$_cdt_unchanged_patterns"; then + _cdt_only_copy="true" + fi + # Copy unskipped files into $_cdt_destdir. + if [ -n "$_cdt_skip_copy" ]; then + echo " Ignoring: $file" + else + _cdt_subdir=${file%/*} + if [ "$_cdt_subdir" != "$file" ]; then + mkdir -p "$_cdt_destdir/$_cdt_subdir" + fi + # Check for marked hard embedded libraries + if [[ -z $_cdt_external ]]; then + _cdt_external_slug= + if [[ $_cdt_source_file == *".lua" ]] && _cdt_external_slug=$( grep -io "@curseforge-project-slug[[:blank:]]*:[[:blank:]]*[^@]\+@" "$_cdt_source_file"); then + _cdt_external_slug="${_cdt_external_slug//[[:blank:]@]/}" + _cdt_external_slug="${_cdt_external_slug##*:}" + if [[ ${_cdt_external_slug,,} == "${package,,}" ]]; then + _cdt_external_slug= + fi + if [[ -n $_cdt_external_slug ]]; then + relations["${_cdt_external_slug,,}"]="embeddedLibrary" + fi + fi + fi + # Check if the file matches a pattern for keyword replacement. + if [ -n "$_cdt_only_copy" ] || ! match_pattern "$file" "*.lua:*.md:*.toc:*.txt:*.xml"; then + echo " Copying: $file (unchanged)${_cdt_external_slug:+(embedded: "$_cdt_external_slug")}" + cp "$_cdt_source_file" "$_cdt_destdir/$_cdt_subdir" + else + _cdt_file_gametype="$_cdt_gametype" + # Set the filters for replacement based on file extension. + _cdt_filters="vcs_filter" + case $file in + *.lua) + [[ -n $_cdt_do_not_package ]] && _cdt_filters+="|do_not_package_filter lua" + [[ -n $_cdt_debug ]] && _cdt_filters+="|lua_filter debug" + [[ -n $_cdt_alpha ]] && _cdt_filters+="|lua_filter alpha" + if [[ -n $_cdt_file_gametype ]]; then + [[ $_cdt_file_gametype != "retail" ]] && _cdt_filters+="|lua_filter version-retail|lua_filter retail" + [[ $_cdt_file_gametype != "classic" ]] && _cdt_filters+="|lua_filter version-classic|lua_filter version-vanilla" + [[ $_cdt_file_gametype != "bcc" ]] && _cdt_filters+="|lua_filter version-bcc" + [[ $_cdt_file_gametype != "wrath" ]] && _cdt_filters+="|lua_filter version-wrath" + [[ $_cdt_file_gametype != "cata" ]] && _cdt_filters+="|lua_filter version-cata" + [[ $_cdt_file_gametype != "mists" ]] && _cdt_filters+="|lua_filter version-mists" + [[ $_cdt_file_gametype != "titan" ]] && _cdt_filters+="|lua_filter version-titan" + [[ $_cdt_file_gametype != "forever" ]] && _cdt_filters+="|lua_filter version-forever" + fi + [[ -n $_cdt_localization ]] && grep -q "@localization" "$_cdt_source_file" && _cdt_filters+="|localization_filter" + ;; + *.xml) + [[ -n $_cdt_do_not_package ]] && _cdt_filters+="|do_not_package_filter xml" + [[ -n $_cdt_nolib ]] && _cdt_filters+="|xml_filter no-lib-strip" + [[ -n $_cdt_debug ]] && _cdt_filters+="|xml_filter debug" + [[ -n $_cdt_alpha ]] && _cdt_filters+="|xml_filter alpha" + if [[ -n $_cdt_file_gametype ]]; then + [[ $_cdt_file_gametype != "retail" ]] && _cdt_filters+="|xml_filter version-retail|xml_filter retail" + [[ $_cdt_file_gametype != "classic" ]] && _cdt_filters+="|xml_filter version-classic|xml_filter version-vanilla" + [[ $_cdt_file_gametype != "bcc" ]] && _cdt_filters+="|xml_filter version-bcc" + [[ $_cdt_file_gametype != "wrath" ]] && _cdt_filters+="|xml_filter version-wrath" + [[ $_cdt_file_gametype != "cata" ]] && _cdt_filters+="|xml_filter version-cata" + [[ $_cdt_file_gametype != "mists" ]] && _cdt_filters+="|xml_filter version-mists" + [[ $_cdt_file_gametype != "titan" ]] && _cdt_filters+="|xml_filter version-titan" + [[ $_cdt_file_gametype != "forever" ]] && _cdt_filters+="|xml_filter version-forever" + fi + ;; + *.toc) + # We only care about processing project TOC files + if [[ -n ${toc_interfaces["$_cdt_source_file"]} ]]; then + # Set the interface values + _cdt_toc_dir="${_cdt_source_file%/*}" + set_info_toc_interface "$_cdt_source_file" "${toc_root_paths["$_cdt_toc_dir"]}" + # Add build type filters + _cdt_filters+="|do_not_package_filter toc" + [ -n "$_cdt_nolib" ] && _cdt_filters+="|toc_filter no-lib-strip true" # leave the tokens in the file normally + _cdt_filters+="|toc_filter debug ${_cdt_debug}" + _cdt_filters+="|toc_filter alpha ${_cdt_alpha}" + # Add game type filters, skipping if multi-version + if [[ -z $_cdt_file_gametype && -n $_cdt_split && -n $si_game_root_interface ]] && ! is_toc_multiple_types "$si_game_root_interface" ; then + toc_to_type "$si_game_root_interface" "_cdt_file_gametype" + fi + if [[ -n "$_cdt_file_gametype" ]]; then + _cdt_filters+="|toc_filter retail $([[ "$_cdt_file_gametype" != "retail" ]] && echo "true")" + _cdt_filters+="|toc_filter version-retail $([[ "$_cdt_file_gametype" != "retail" ]] && echo "true")" + _cdt_filters+="|toc_filter version-classic $([[ "$_cdt_file_gametype" != "classic" ]] && echo "true")" + _cdt_filters+="|toc_filter version-vanilla $([[ "$_cdt_file_gametype" != "classic" ]] && echo "true")" + _cdt_filters+="|toc_filter version-bcc $([[ "$_cdt_file_gametype" != "bcc" ]] && echo "true")" + _cdt_filters+="|toc_filter version-wrath $([[ "$_cdt_file_gametype" != "wrath" ]] && echo "true")" + _cdt_filters+="|toc_filter version-cata $([[ "$_cdt_file_gametype" != "cata" ]] && echo "true")" + _cdt_filters+="|toc_filter version-mists $([[ "$_cdt_file_gametype" != "mists" ]] && echo "true")" + _cdt_filters+="|toc_filter version-titan $([[ "$_cdt_file_gametype" != "titan" ]] && echo "true")" + _cdt_filters+="|toc_filter version-forever $([[ "$_cdt_file_gametype" != "forever" ]] && echo "true")" + fi + # Rewrite the interface line if necessary + _cdt_filters+="|toc_interface_filter '${si_game_type_interface_all[${_cdt_file_gametype:- }]}' '${toc_root_interface["$_cdt_source_file"]}'" + # Do localized key replacements (this is really inefficient and should probably be deprecated) + [ -n "$_cdt_localization" ] && grep -q "@localization" "$_cdt_source_file" && _cdt_filters+="|localization_filter" + fi + ;; + esac + + # Set the filter for normalizing line endings. + _cdt_filters+="|line_ending_filter" + + # Set version control values for the file. + set_info_file "$_cdt_source_file" + + echo " Copying: $file${_cdt_external_slug:+ (embedded: "$_cdt_external_slug")}" + + # Make sure we're not causing any surprises + if [[ -z $_cdt_file_gametype && ( $file == *".lua" || $file == *".xml" || ( -z $_cdt_external && $file == *".toc" ) ) ]] && grep -q '@\(non-\)\?version-\(retail\|classic\|vanilla\|bcc\|wrath\|cata\|mists\|titan\|forever\)@' "$_cdt_source_file"; then + echo " Error! Build type version keywords are not allowed in a multi-version build." >&2 + echo " These should be replaced with lua conditional statements and/or TOC loading conditions:" >&2 + grep -n '@\(non-\)\?version-\(retail\|classic\|vanilla\|bcc\|wrath\|cata\|mists\|titan\|forever\)@' "$_cdt_source_file" | sed 's/^/ /' >&2 + echo " See https://warcraft.wiki.gg/wiki/WOW_PROJECT_ID" >&2 + echo " See https://warcraft.wiki.gg/wiki/TOC_format" >&2 + exit 1 + fi + + set -o pipefail + eval < "$_cdt_source_file" "$_cdt_filters" 3>&1 > "$_cdt_destdir/$file" || exit 1 + + # Create game type specific TOCs + if [[ -n $_cdt_split && -n ${toc_interfaces["$_cdt_source_file"]} ]]; then + local toc_version new_file + local root_toc_version="$si_game_root_interface" + for type in "${!si_game_type_interface[@]}"; do + toc_version="${si_game_type_interface[$type]}" + new_file="${file%.toc}" + case $type in + retail) new_file+="_Mainline.toc" ;; + classic) new_file+="_Vanilla.toc" ;; + bcc) new_file+="_TBC.toc" ;; + wrath) new_file+="_Wrath.toc" ;; + cata) new_file+="_Cata.toc" ;; + mists) new_file+="_Mists.toc" ;; + forever) new_file+="_Camelot.toc" ;; + # titan) new_file+="_Wrath.toc" ;; + esac + + echo " Creating $new_file [${toc_version//:/, }]" + + _cdt_filters="vcs_filter" + _cdt_filters+="|do_not_package_filter toc" + [ -n "$_cdt_nolib" ] && _cdt_filters+="|toc_filter no-lib-strip true" # leave the tokens in the file normally + _cdt_filters+="|toc_filter debug true" + _cdt_filters+="|toc_filter alpha ${_cdt_alpha}" + _cdt_filters+="|toc_filter retail $([[ "$type" != "retail" ]] && echo "true")" + _cdt_filters+="|toc_filter version-retail $([[ "$type" != "retail" ]] && echo "true")" + _cdt_filters+="|toc_filter version-classic $([[ "$type" != "classic" ]] && echo "true")" + _cdt_filters+="|toc_filter version-vanilla $([[ "$type" != "classic" ]] && echo "true")" + _cdt_filters+="|toc_filter version-bcc $([[ "$type" != "bcc" ]] && echo "true")" + _cdt_filters+="|toc_filter version-wrath $([[ "$type" != "wrath" ]] && echo "true")" + _cdt_filters+="|toc_filter version-cata $([[ "$type" != "cata" ]] && echo "true")" + _cdt_filters+="|toc_filter version-mists $([[ "$type" != "mists" ]] && echo "true")" + _cdt_filters+="|toc_filter version-titan $([[ "$type" != "titan" ]] && echo "true")" + _cdt_filters+="|toc_filter version-forever $([[ "$type" != "forever" ]] && echo "true")" + _cdt_filters+="|toc_interface_filter '$toc_version' '$root_toc_version'" + _cdt_filters+="|line_ending_filter" + + eval < "$_cdt_source_file" "$_cdt_filters" 3>&1 > "$_cdt_destdir/$new_file" + done + + if [[ -z $root_toc_version ]]; then + echo " Removing $file (no base interface)" + rm -f "$_cdt_destdir/$file" + fi + fi + fi + fi + fi + done < <( cd "$_cdt_srcdir" && eval "$_cdt_find_cmd" ) + if [ -z "$_external_dir" ]; then + end_group "copy" + fi +} + +if [ -z "$skip_copying" ]; then + cdt_args="-dp" + [ "$file_type" != "alpha" ] && cdt_args+="a" + [ -z "$skip_localization" ] && cdt_args+="l" + [ -n "$nolib" ] && cdt_args+="n" + [ -n "$split" ] && cdt_args+="S" + [ -n "$game_type" ] && cdt_args+=" -g $game_type" + [ -n "$ignore" ] && cdt_args+=" -i \"$ignore\"" + [ -n "$unchanged" ] && cdt_args+=" -u \"$unchanged\"" + eval copy_directory_tree "$cdt_args" "\"$topdir\"" "\"$pkgdir\"" +fi + +# Reset ignore and parse pkgmeta ignores again to handle ignoring external paths +ignore= +unchanged= +parse_ignore "$pkgmeta_file" + +### +### Process .pkgmeta again to perform any pre-move-folders actions. +### + +# Checkout the external into a ".checkout" subdirectory of the final directory. +checkout_external() { + local _external_dir="$1" + local _external_uri="$2" + local _external_tag="$3" + local _external_type="$4" + # shellcheck disable=SC2034 # _external_slug unused until we can easily fetch the project id + local _external_slug="$5" + local _external_checkout_type="$6" + local _external_path="$7" + + _cqe_checkout_dir="$pkgdir/$_external_dir/.checkout" + if [[ -d $_cqe_checkout_dir ]]; then + # cleanup in case there was an aborted attempt to checkout + rm -rf "$_cqe_checkout_dir" + fi + mkdir -p "$_cqe_checkout_dir" + # The .pkgmeta to read "ignore" and "plain-copy" from, and the path its patterns are matched in. + local _cqe_pkgmeta="$_cqe_checkout_dir/.pkgmeta" + local _cqe_pkgmeta_path="$_external_path" + if [ "$_external_type" = "git" ]; then + if [ -z "$_external_tag" ]; then + echo "Fetching latest version of external $_external_uri" + retry git clone -q --depth 1 "$_external_uri" "$_cqe_checkout_dir" || return 1 + elif [ "$_external_tag" != "latest" ]; then + echo "Fetching $_external_checkout_type \"$_external_tag\" from external $_external_uri" + if [ "$_external_checkout_type" = "commit" ]; then + retry git clone -q "$_external_uri" "$_cqe_checkout_dir" || return 1 + git -C "$_cqe_checkout_dir" checkout -q "$_external_tag" || return 1 + else + git -c advice.detachedHead=false clone -q --depth 1 --branch "$_external_tag" "$_external_uri" "$_cqe_checkout_dir" || return 1 + fi + else # [ "$_external_tag" = "latest" ]; then + retry git clone -q --depth 50 "$_external_uri" "$_cqe_checkout_dir" || return 1 + _external_tag=$( git -C "$_cqe_checkout_dir" for-each-ref refs/tags --sort=-creatordate --format=%\(refname:short\) --count=1 ) + if [ -n "$_external_tag" ]; then + echo "Fetching tag \"$_external_tag\" from external $_external_uri" + git -C "$_cqe_checkout_dir" checkout -q "$_external_tag" || return 1 + else + echo "Fetching latest version of external $_external_uri" + fi + fi + + # pull submodules + git -C "$_cqe_checkout_dir" submodule -q update --init --recursive || return 1 + + set_info_git "$_cqe_checkout_dir" + echo "Checked out $( git -C "$_cqe_checkout_dir" describe --always --tags --abbrev=7 --long )" #$si_project_abbreviated_hash + elif [ "$_external_type" = "svn" ]; then + if [[ $external_uri == *"/trunk" ]]; then + _cqe_svn_trunk_url=$_external_uri + _cqe_svn_subdir= + else + _cqe_svn_trunk_url="${_external_uri%/trunk/*}/trunk" + _cqe_svn_subdir=${_external_uri#${_cqe_svn_trunk_url}/} + fi + _cqe_svn_root_url=$_cqe_svn_trunk_url + + if [ -z "$_external_tag" ]; then + echo "Fetching latest version of external $_external_uri" + retry_svn_checkout "$_external_uri" "$_cqe_checkout_dir" || return 1 + else + _cqe_svn_tag_url="${_cqe_svn_trunk_url%/trunk}/tags" + if [ "$_external_tag" = "latest" ]; then + _external_tag=$( retry svn log --verbose --limit 1 "$_cqe_svn_tag_url" 2>/dev/null | awk '/^ A \/tags\// { print $2; exit }' | awk -F/ '{ print $3 }' ) + if [ -z "$_external_tag" ]; then + _external_tag="latest" + fi + fi + if [ "$_external_tag" = "latest" ]; then + echo "No tags found in $_cqe_svn_tag_url" + echo "Fetching latest version of external $_external_uri" + retry_svn_checkout "$_external_uri" "$_cqe_checkout_dir" || return 1 + else + _cqe_external_uri="${_cqe_svn_tag_url}/$_external_tag" + _cqe_svn_root_url=$_cqe_external_uri + if [ -n "$_cqe_svn_subdir" ]; then + _cqe_external_uri="${_cqe_external_uri}/$_cqe_svn_subdir" + fi + echo "Fetching tag \"$_external_tag\" from external $_cqe_external_uri" + retry_svn_checkout "$_cqe_external_uri" "$_cqe_checkout_dir" || return 1 + fi + fi + set_info_svn "$_cqe_checkout_dir" || return 1 + echo "Checked out r$si_project_revision" + + # A checkout of a subdirectory leaves the .pkgmeta in the root behind, so fetch it. + if [[ $_external_uri == *"/trunk/"* && ! -f "$_cqe_pkgmeta" ]]; then + retry svn_cat_optional "$_cqe_svn_root_url/.pkgmeta" "$_cqe_pkgmeta" || return 1 + _cqe_pkgmeta_path="$_cqe_svn_subdir${_external_path:+/$_external_path}" + fi + elif [ "$_external_type" = "hg" ]; then + if [ -z "$_external_tag" ]; then + echo "Fetching latest version of external $_external_uri" + retry hg clone -q "$_external_uri" "$_cqe_checkout_dir" || return 1 + elif [ "$_external_tag" != "latest" ]; then + echo "Fetching $_external_checkout_type \"$_external_tag\" from external $_external_uri" + retry hg clone -q --updaterev "$_external_tag" "$_external_uri" "$_cqe_checkout_dir" || return 1 + else # [ "$_external_tag" = "latest" ]; then + retry hg clone -q "$_external_uri" "$_cqe_checkout_dir" || return 1 + _external_tag=$( hg --cwd "$_cqe_checkout_dir" log -r . --template '{latesttag}' ) + if [ -n "$_external_tag" ]; then + echo "Fetching tag \"$_external_tag\" from external $_external_uri" + hg --cwd "$_cqe_checkout_dir" update -q "$_external_tag" + else + echo "Fetching latest version of external $_external_uri" + fi + fi + set_info_hg "$_cqe_checkout_dir" + echo "Checked out r$si_project_revision" + elif [ "$_external_type" = "zip" ]; then + echo "Fetching archive $_external_uri" + local _cqe_zip_file="$_cqe_checkout_dir.zip" + local _cqe_zip_result=0 + local _cqe_zip_root + if curl -sSfL --retry 3 --retry-delay 10 -o "$_cqe_zip_file" "$_external_uri"; then + unzip -qq "$_cqe_zip_file" -d "$_cqe_checkout_dir" || _cqe_zip_result=$? + # unzip exits with 1 for a warning, where everything was still extracted + [ "$_cqe_zip_result" -eq 1 ] && _cqe_zip_result=0 + else + _cqe_zip_result=$? + fi + rm -f "$_cqe_zip_file" + [ "$_cqe_zip_result" -eq 0 ] || return 1 + + # Use a lone top-level directory as the root, like the one a GitHub archive is wrapped in. + _cqe_zip_root=$( find "$_cqe_checkout_dir" -mindepth 1 -maxdepth 1 ) + if [ -d "$_cqe_zip_root" ]; then + _external_path="${_cqe_zip_root##*/}${_external_path:+/$_external_path}" + fi + + # An archive is used as it is, so copy all of its files without processing them. + _cqe_pkgmeta= + unchanged="${unchanged:+$unchanged:}$_external_dir/*" + echo "Extracted $( find "$_cqe_checkout_dir" -type f | wc -l | tr -d ' ' ) files" + else + echo "Unknown external: $_external_uri" >&2 + return 1 + fi + # Copy the checkout into the proper external directory. + ( + cd "$_cqe_checkout_dir" || return 1 + # Set the slug for external localization, if needed. + # Note: We don't actually do localization since we need the project id and + # the only way to convert slug->id would be to scrape the project page :\ + slug= #$_external_slug + project_site= + package= + if [[ "$_external_uri" == *"wowace.com"* || "$_external_uri" == *"curseforge.com"* ]]; then + project_site="https://wow.curseforge.com" + fi + + # If a .pkgmeta file is present, process it for "ignore" and "plain-copy" lists. + parse_ignore "$_cqe_pkgmeta" "$_external_dir" "$_cqe_pkgmeta_path" + if [ -n "$_external_path" ]; then + echo "Changing to /$_external_path" + _cqe_checkout_dir="$_cqe_checkout_dir/$_external_path" + if [ ! -d "$_cqe_checkout_dir" ]; then + echo " ERROR! Root directory set by \"path\" does not exist." >&2 + return 1 + fi + cd "$_cqe_checkout_dir" || return 1 + fi + copy_directory_tree -dnpe -i "$ignore" -u "$unchanged" "$_cqe_checkout_dir" "$pkgdir/$_external_dir" + ) || return 1 + # Remove the ".checkout" subdirectory containing the full checkout. + if [ -d "$_cqe_checkout_dir" ]; then + rm -fr "$_cqe_checkout_dir" + fi +} + +external_pids=() + +external_dir= +external_uri= +external_tag= +external_type= +external_slug= +external_checkout_type= +external_path= +process_external() { + if [ -n "$external_dir" ] && [ -n "$external_uri" ] && [ -z "$skip_externals" ]; then + echo "Fetching external: $external_dir" + + external_uri=${external_uri%%#*} # strip trailing comment + external_uri=${external_uri% *} # strip trailing space + external_uri=${external_uri%/} # strip trailing slash + + # convert old curse repo urls + case $external_uri in + *git.curseforge.com*|*git.wowace.com*) + external_type="git" + # git://git.curseforge.com/wow/$slug/mainline.git -> https://repos.curseforge.com/wow/$slug + external_uri=${external_uri%/mainline.git} + external_uri="https://repos${external_uri#*://git}" + ;; + *svn.curseforge.com*|*svn.wowace.com*) + external_type="svn" + # svn://svn.curseforge.com/wow/$slug/mainline/trunk -> https://repos.curseforge.com/wow/$slug/trunk + external_uri=${external_uri/\/mainline/} + external_uri="https://repos${external_uri#*://svn}" + ;; + *hg.curseforge.com*|*hg.wowace.com*) + external_type="hg" + # http://hg.curseforge.com/wow/$slug/mainline -> https://repos.curseforge.com/wow/$slug + external_uri=${external_uri%/mainline} + external_uri="https://repos${external_uri#*://hg}" + ;; + svn:*) + # just in case + external_type="svn" + ;; + *.zip|*.zip\?*) + if [ -z "$external_type" ]; then + external_type="zip" + fi + ;; + *) + if [ -z "$external_type" ]; then + external_type="git" + fi + ;; + esac + + if [[ $external_uri == "https://repos.curseforge.com/wow/"* || $external_uri == "https://repos.wowace.com/wow/"* ]]; then + if [ -z "$external_slug" ]; then + external_slug=${external_uri#*/wow/} + external_slug=${external_slug%%/*} + fi + + # check if the repo is svn + local _svn_path=${external_uri#*/wow/$external_slug/} + if [[ "$_svn_path" == "trunk"* ]]; then + external_type="svn" + elif [[ "$_svn_path" == "tags/"* ]]; then + external_type="svn" + # change the tag path into the trunk path and use the tag var so it gets logged as a tag + external_tag=${_svn_path#tags/} + external_tag=${external_tag%%/*} + external_uri="${external_uri%/tags*}/trunk${_svn_path#tags/$external_tag}" + fi + fi + + if [[ $external_type == "git" ]]; then + if ! command -v git &>/dev/null; then + echo " ERROR! \"$external_uri\" is a git repository, but git is not available." >&2 + exit 1 + fi + # check for subpath in urls we know the structure of + if [[ -n $external_slug && $external_uri == *"$external_slug/"* ]]; then + # CF: https://repos.curseforge.com/wow/libdothings-1-0/LibDoThings-1.0 + external_path=${external_uri#*/wow/$external_slug/} + external_uri=${external_uri%/$external_path*} + elif [[ $external_uri == "https://github.com/"*/*/* ]]; then + # GitHub: https://github.com/nebularg/LibDoThings-1.0.git/LibDoThings-1.0 + external_path=${external_uri#*.com/*/*/} + external_uri=${external_uri%/$external_path*} + fi + elif [[ $external_type == "svn" ]]; then + if ! command -v svn &>/dev/null; then + echo " ERROR! \"$external_uri\" is a subversion repository, but svn is not available." >&2 + exit 1 + fi + elif [[ $external_type == "hg" ]]; then + if ! command -v hg &>/dev/null; then + echo " ERROR! \"$external_uri\" is a mercurial repository, but hg is not available." >&2 + exit 1 + fi + elif [[ $external_type == "zip" ]]; then + if ! command -v unzip &>/dev/null; then + echo " ERROR! \"$external_uri\" is a zip archive, but unzip is not available." >&2 + exit 1 + fi + fi + + if [ -n "$external_slug" ]; then + relations["${external_slug,,}"]="embeddedLibrary" + fi + + checkout_external "$external_dir" "$external_uri" "$external_tag" "$external_type" "$external_slug" "$external_checkout_type" "$external_path" &> "$releasedir/.$BASHPID.externalout" & + external_pids+=($!) + fi + external_dir= + external_uri= + external_tag= + external_type= + external_slug= + external_checkout_type= + external_path= +} + +# Don't leave extra files around if exited early +kill_externals() { + rm -f "$releasedir"/.*.externalout + kill 0 +} +trap kill_externals INT + +if [ -z "$skip_externals" ] && [ -f "$pkgmeta_file" ]; then + yaml_eof= + while [ -z "$yaml_eof" ]; do + IFS='' read -r yaml_line || yaml_eof="true" + # Skip commented out lines. + if [[ $yaml_line =~ ^[[:space:]]*\# ]]; then + continue + fi + # Strip any trailing CR character. + yaml_line="${yaml_line%"$carriage_return"}" + + case $yaml_line in + [!\ ]*:*) + # Started a new section, so checkout any queued externals. + process_external + # Split $yaml_line into a $yaml_key, $yaml_value pair. + yaml_keyvalue "$yaml_line" + # Set the $pkgmeta_phase for stateful processing. + pkgmeta_phase="$yaml_key" + ;; + " "*) + yaml_line="${yaml_line#"${yaml_line%%[! ]*}"}" # trim leading whitespace + case $yaml_line in + "- "*) + ;; + *:*) + # Split $yaml_line into a $yaml_key, $yaml_value pair. + yaml_keyvalue "$yaml_line" + case $pkgmeta_phase in + externals) + case $yaml_key in + url) external_uri="$yaml_value" ;; + tag) + external_tag="$yaml_value" + external_checkout_type="$yaml_key" + ;; + branch) + external_tag="$yaml_value" + external_checkout_type="$yaml_key" + ;; + commit) + external_tag="$yaml_value" + external_checkout_type="$yaml_key" + ;; + type) external_type="$yaml_value" ;; + curse-slug) external_slug="$yaml_value" ;; + path) external_path="$yaml_value" ;; + *) + # Started a new external, so checkout any queued externals. + process_external + + external_dir="$yaml_key" + nolib_exclude="$nolib_exclude $pkgdir/$external_dir/*" + if [ -n "$yaml_value" ]; then + external_uri="$yaml_value" + # Immediately checkout this fully-specified external. + process_external + fi + ;; + esac + ;; + esac + ;; + esac + ;; + esac + done < "$pkgmeta_file" + # Reached end of file, so checkout any remaining queued externals. + process_external + + if [ ${#external_pids[*]} -gt 0 ]; then + echo + echo "Waiting for externals to finish..." + echo + + while [ ${#external_pids[*]} -gt 0 ]; do + wait -n + for i in ${!external_pids[*]}; do + pid=${external_pids[i]} + if ! kill -0 "$pid" 2>/dev/null; then + _external_output="$releasedir/.$pid.externalout" + if ! wait "$pid"; then + _external_error=1 + # wrap each line with a bright red color code + awk '{ printf "\033[01;31m%s\033[0m\n", $0 }' "$_external_output" + echo + else + start_group "$( head -n1 "$_external_output" )" "external.$pid" + tail -n+2 "$_external_output" + end_group "external.$pid" + fi + rm -f "$_external_output" 2>/dev/null + unset 'external_pids[i]' + fi + done + done + + if [ -n "$_external_error" ]; then + echo + echo "There was an error fetching externals :(" >&2 + exit 1 + fi + fi +fi + +# Restore the signal handlers +trap - INT + +### +### Create the changelog of commits since the previous release tag. +### + +if [ -z "$project" ]; then + project="$package" +fi + +# Create a changelog in the package directory if the source directory does +# not contain a manual changelog. + +changelog_path= +if [[ -n "$manual_changelog" && -f "$topdir/$changelog" ]]; then + if [[ -f "$pkgdir/$changelog" ]]; then + # use the processed copy + changelog_path="$pkgdir/$changelog" + else + changelog_path="$topdir/$changelog" + fi + start_group "Using manual changelog $changelog" "changelog" + head -n7 "$changelog_path" + [ "$( wc -l < "$changelog_path" )" -gt 7 ] && echo "..." + end_group "changelog" + + # Convert Markdown to BBCode (with HTML as an intermediary) for sending to WoWInterface + # Requires pandoc (http://pandoc.org/) + if [ "$changelog_markup" = "markdown" ] && [ -n "$wowi_convert_changelog" ] && command -v pandoc &>/dev/null; then + wowi_changelog="$releasedir/WOWI-$project_version-CHANGELOG.txt" + pandoc --wrap=none -f commonmark -t html "$changelog_path" | sed \ + -e 's/<\(\/\)\?\(b\|i\|u\)>/[\1\2]/g' \ + -e 's/<\(\/\)\?em>/[\1i]/g' \ + -e 's/<\(\/\)\?strong>/[\1b]/g' \ + -e 's/]*>/[list]/g' -e 's/]*>/[list="1"]/g' \ + -e 's/<\/[ou]l>/[\/list]\n/g' \ + -e 's/
  • /[*]/g' -e 's/

  • /[*]/g' -e 's/<\/p><\/li>//g' -e 's/<\/li>//g' \ + -e 's/\[\*\]\[ \] /[*]☐ /g' -e 's/\[\*\]\[[xX]\] /[*]☒ /g' \ + -e 's/]*>/[size="6"]/g' -e 's/]*>/[size="5"]/g' -e 's/]*>/[size="4"]/g' \ + -e 's/]*>/[size="3"]/g' -e 's/]*>/[size="3"]/g' -e 's/]*>/[size="3"]/g' \ + -e 's/<\/h[1-6]>/[\/size]\n/g' \ + -e 's/
    /[quote]/g' -e 's/<\/blockquote>/[\/quote]\n/g' \ + -e 's/
    ]*>
    /[highlight="lua"]/g' -e 's/<\/code><\/pre><\/div>/[\/highlight]\n/g' \
    +			-e 's/
    /[code]/g' -e 's/<\/code><\/pre>/[\/code]\n/g' \
    +			-e 's//[font="monospace"]/g' -e 's/<\/code>/[\/font]/g' \
    +			-e 's/]*>/[url="\1"]/g' -e 's/<\/a>/\[\/url]/g' \
    +			-e 's/]*>/[img]\1[\/img]/g' \
    +			-e 's/
    /_____________________________________________________________________________\n/g' \ + -e 's/<\/p>/\n/g' \ + -e '/^<[^>]\+>$/d' -e 's/<[^>]\+>//g' \ + -e 's/"/"/g' \ + -e 's/&/&/g' \ + -e 's/<//g' \ + -e "s/'/'/g" \ + | line_ending_filter > "$wowi_changelog" + fi +else + if [ -n "$manual_changelog" ]; then + echo "Warning! Could not find a manual changelog at $topdir/$changelog" + manual_changelog= + fi + changelog="CHANGELOG.md" + changelog_markup="markdown" + changelog_path="$pkgdir/$changelog" + + if [ -n "$wowi_gen_changelog" ] && [ -z "$wowi_convert_changelog" ]; then + wowi_markup="markdown" + fi + + start_group "Generating changelog of commits into $changelog" "changelog" + + _changelog_range= + if [ "$repository_type" = "git" ]; then + changelog_url= + changelog_version= + changelog_previous="[Previous Releases](${project_github_url}/releases)" + changelog_url_wowi= + changelog_version_wowi= + changelog_previous_wowi="[url=${project_github_url}/releases]Previous Releases[/url]" + if [ -z "$previous_version" ] && [ -z "$tag" ]; then + # no range, show all commits up to ours + changelog_url="[Full Changelog](${project_github_url}/commits/${project_hash})" + changelog_version="[${project_version}](${project_github_url}/tree/${project_hash})" + changelog_url_wowi="[url=${project_github_url}/commits/${project_hash}]Full Changelog[/url]" + changelog_version_wowi="[url=${project_github_url}/tree/${project_hash}]${project_version}[/url]" + _changelog_range="$project_hash" + elif [ -z "$previous_version" ] && [ -n "$tag" ]; then + # first tag, show all commits upto it + changelog_url="[Full Changelog](${project_github_url}/commits/${tag})" + changelog_version="[${project_version}](${project_github_url}/tree/${tag})" + changelog_url_wowi="[url=${project_github_url}/commits/${tag}]Full Changelog[/url]" + changelog_version_wowi="[url=${project_github_url}/tree/${tag}]${project_version}[/url]" + _changelog_range="$tag" + elif [ -n "$previous_version" ] && [ -z "$tag" ]; then + # compare between last tag and our commit + changelog_url="[Full Changelog](${project_github_url}/compare/${previous_version}...${project_hash})" + changelog_version="[$project_version](${project_github_url}/tree/${project_hash})" + changelog_url_wowi="[url=${project_github_url}/compare/${previous_version}...${project_hash}]Full Changelog[/url]" + changelog_version_wowi="[url=${project_github_url}/tree/${project_hash}]${project_version}[/url]" + _changelog_range="$previous_version..$project_hash" + elif [ -n "$previous_version" ] && [ -n "$tag" ]; then + # compare between last tag and our tag + changelog_url="[Full Changelog](${project_github_url}/compare/${previous_version}...${tag})" + changelog_version="[$project_version](${project_github_url}/tree/${tag})" + changelog_url_wowi="[url=${project_github_url}/compare/${previous_version}...${tag}]Full Changelog[/url]" + changelog_version_wowi="[url=${project_github_url}/tree/${tag}]${project_version}[/url]" + _changelog_range="$previous_version..$tag" + fi + # lazy way out + if [ -z "$project_github_url" ]; then + changelog_url= + changelog_version=$project_version + changelog_previous= + changelog_url_wowi= + changelog_version_wowi="[color=orange]${project_version}[/color]" + changelog_previous_wowi= + elif [ -z "$github_token" ]; then + # not creating releases :( + changelog_previous= + changelog_previous_wowi= + fi + changelog_date=$( TZ='' printf "%(%Y-%m-%d)T" "$project_timestamp" ) + + cat <<- EOF | line_ending_filter > "$changelog_path" + # $project + + ## $changelog_version ($changelog_date) + $changelog_url $changelog_previous + + EOF + # shellcheck disable=SC2016 # don't expand backticks + git -C "$topdir" log "$_changelog_range" --pretty=format:"###%B" \ + | sed -e 's/^/ /g' -e 's/^ *$//g' -e 's/^ ###/- /g' -e 's/$/ /' \ + -e ':a;s/^\(\(`[^`]*`\|[^`_]*\)*\)_/\1\\###/;ta' -e 's/###/_/g' \ + -e 's/\[ci skip\]//g' -e 's/\[skip ci\]//g' \ + -e '/git-svn-id:/d' -e '/^[[:space:]]*This reverts commit [0-9a-f]\{40\}\.[[:space:]]*$/d' \ + -e '/^[[:space:]]*$/d' \ + | line_ending_filter >> "$changelog_path" + + # WoWI uses BBCode, generate something usable to post to the site + # the file is deleted on successful upload + if [ -n "$addonid" ] && [ -n "$tag" ] && [ -n "$wowi_gen_changelog" ] && [ "$wowi_markup" = "bbcode" ]; then + wowi_changelog="$releasedir/WOWI-$project_version-CHANGELOG.txt" + cat <<- EOF | line_ending_filter > "$wowi_changelog" + [size=5]${project}[/size] + [size=4]${changelog_version_wowi} (${changelog_date})[/size] + ${changelog_url_wowi} ${changelog_previous_wowi} + [list] + EOF + git -C "$topdir" log "$_changelog_range" --pretty=format:"###%B" \ + | sed -e 's/^/ /g' -e 's/^ *$//g' -e 's/^ ###/[*]/g' \ + -e 's/\[ci skip\]//g' -e 's/\[skip ci\]//g' \ + -e '/git-svn-id:/d' -e '/^[[:space:]]*This reverts commit [0-9a-f]\{40\}\.[[:space:]]*$/d' \ + -e '/^[[:space:]]*$/d' \ + | line_ending_filter >> "$wowi_changelog" + echo "[/list]" | line_ending_filter >> "$wowi_changelog" + fi + + elif [ "$repository_type" = "svn" ]; then + if [ -n "$previous_revision" ]; then + _changelog_range="-r$project_revision:$previous_revision" + else + _changelog_range="-rHEAD:1" + fi + changelog_date=$( TZ='' printf "%(%Y-%m-%d)T" "$project_timestamp" ) + + cat <<- EOF | line_ending_filter > "$changelog_path" + # $project + + ## $project_version ($changelog_date) + + EOF + _svn_changelog=$( retry svn log "$topdir" "$_changelog_range" --xml ) + # shellcheck disable=SC2016 # don't expand backticks + echo "$_svn_changelog" \ + | awk '//,/<\/msg>/' \ + | sed -e 's//###/g' -e 's/<\/msg>//g' \ + -e 's/^/ /g' -e 's/^ *$//g' -e 's/^ ###/- /g' -e 's/$/ /' \ + -e ':a;s/^\(\(`[^`]*`\|[^`_]*\)*\)_/\1\\###/;ta' -e 's/###/_/g' \ + -e 's/\[ci skip\]//g' -e 's/\[skip ci\]//g' \ + -e '/^[[:space:]]*$/d' \ + | line_ending_filter >> "$changelog_path" + + # WoWI uses BBCode, generate something usable to post to the site + # the file is deleted on successful upload + if [ -n "$addonid" ] && [ -n "$tag" ] && [ -n "$wowi_gen_changelog" ] && [ "$wowi_markup" = "bbcode" ]; then + wowi_changelog="$releasedir/WOWI-$project_version-CHANGELOG.txt" + cat <<- EOF | line_ending_filter > "$wowi_changelog" + [size=5]${project}[/size] + [size=4][color=orange]${project_version}[/color] (${changelog_date})[/size] + + [list] + EOF + echo "$_svn_changelog" \ + | awk '//,/<\/msg>/' \ + | sed -e 's//###/g' -e 's/<\/msg>//g' \ + -e 's/^/ /g' -e 's/^ *$//g' -e 's/^ ###/[*]/g' \ + -e 's/\[ci skip\]//g' -e 's/\[skip ci\]//g' \ + -e '/^[[:space:]]*$/d' \ + | line_ending_filter >> "$wowi_changelog" + echo "[/list]" | line_ending_filter >> "$wowi_changelog" + fi + unset _svn_changelog + + elif [ "$repository_type" = "hg" ]; then + if [ -n "$previous_revision" ]; then + _changelog_range="::$project_revision - ::$previous_revision - filelog(.hgtags)" + else + _changelog_range="." + fi + changelog_date=$( TZ='' printf "%(%Y-%m-%d)T" "$project_timestamp" ) + + cat <<- EOF | line_ending_filter > "$changelog_path" + # $project + + ## $project_version ($changelog_date) + + EOF + hg --cwd "$topdir" log -r "$_changelog_range" --template '- {fill(desc|strip, 76, "", " ")}\n' | line_ending_filter >> "$changelog_path" + + # WoWI uses BBCode, generate something usable to post to the site + # the file is deleted on successful upload + if [ -n "$addonid" ] && [ -n "$tag" ] && [ -n "$wowi_gen_changelog" ] && [ "$wowi_markup" = "bbcode" ]; then + wowi_changelog="$releasedir/WOWI-$project_version-CHANGELOG.txt" + cat <<- EOF | line_ending_filter > "$wowi_changelog" + [size=5]${project}[/size] + [size=4][color=orange]${project_version}[/color] (${changelog_date})[/size] + + [list] + EOF + hg --cwd "$topdir" log "$_changelog_range" --template '[*]{desc|strip|escape}\n' | line_ending_filter >> "$wowi_changelog" + echo "[/list]" | line_ending_filter >> "$wowi_changelog" + fi + fi + + echo "$(<"$changelog_path")" + end_group "changelog" +fi + +### +### Create a license if not present and .pkgmeta requests one. +### + +if [[ -n "$license" && ! -f "$topdir/$license" && -n "$slug" ]]; then + start_group "Saving license as $license" "license" + # this only exists on wowace.com now + if license_text=$( curl -sf --retry 3 --retry-delay 10 "https://www.wowace.com/project/$slug/license" 2>/dev/null ); then + # text is wrapped with \n\n
    \n\t

    \n\t\t ... \n\t

    \n
    \n + echo "$license_text" | sed -e '1,4d' -e '5s/^\s*//' -e '$d' | sed '$d' > "$pkgdir/$license" + head -n10 "$pkgdir/$license" + [[ "$( wc -l < "$pkgdir/$license" )" -gt 10 ]] && echo "..." + else + echo "There was an error saving the license. ($?)" + fi + end_group "license" +fi + +### +### Process .pkgmeta to perform move-folders actions. +### + +if [ -f "$pkgmeta_file" ]; then + yaml_eof= + while [ -z "$yaml_eof" ]; do + IFS='' read -r yaml_line || yaml_eof="true" + # Skip commented out lines. + if [[ $yaml_line =~ ^[[:space:]]*\# ]]; then + continue + fi + # Strip any trailing CR character. + yaml_line=${yaml_line%$carriage_return} + + case $yaml_line in + [!\ ]*:*) + # Split $yaml_line into a $yaml_key, $yaml_value pair. + yaml_keyvalue "$yaml_line" + # Set the $pkgmeta_phase for stateful processing. + pkgmeta_phase=$yaml_key + ;; + " "*) + yaml_line=${yaml_line#"${yaml_line%%[! ]*}"} # trim leading whitespace + case $yaml_line in + "- "*) + ;; + *:*) + # Split $yaml_line into a $yaml_key, $yaml_value pair. + yaml_keyvalue "$yaml_line" + case $pkgmeta_phase in + move-folders) + srcdir="$releasedir/$yaml_key" + destdir="$releasedir/$yaml_value" + if [[ -d "$destdir" && -z "$overwrite" && "$srcdir" != "$destdir/"* ]]; then + rm -fr "$destdir" + fi + if [ -d "$srcdir" ]; then + if [ ! -d "$destdir" ]; then + mkdir -p "$destdir" + fi + echo "Moving $yaml_key to $yaml_value" + mv -f "$srcdir"/* "$destdir" && rm -fr "$srcdir" + zip_root_dirs+=("$yaml_value") + # Copy the license into $destdir if one doesn't already exist. + if [[ -n "$license" && -f "$pkgdir/$license" && ! -f "$destdir/$license" ]]; then + cp -f "$pkgdir/$license" "$destdir/$license" + fi + # Check to see if the base source directory is empty + _mf_basedir="/${yaml_key%/*}" + while [[ -n "$_mf_basedir" && -z "$( ls -A "${releasedir}${_mf_basedir}" )" ]]; do + echo "Removing empty directory ${_mf_basedir#/}" + rm -fr "${releasedir}${_mf_basedir}" + _mf_basedir="${_mf_basedir%/*}" + done + fi + # update external dir + nolib_exclude=${nolib_exclude//$srcdir/$destdir} + # update changelog path + if [[ "$changelog_path" == "$srcdir/"* && -f "$destdir/${changelog_path#"$srcdir/"}" ]]; then + changelog_path="$destdir/${changelog_path#"$srcdir/"}" + fi + ;; + esac + ;; + esac + ;; + esac + done < "$pkgmeta_file" + if [ -n "$srcdir" ]; then + echo + fi +fi + +### +### Create the final zipfile for the addon. +### + +if [ -z "$skip_zipfile" ]; then + archive_version="$project_version" # XXX used for wowi version. should probably switch to label, but the game type gets added on by default :\ + archive_label="$( filename_filter "$label_template" )" + archive_name="$( filename_filter "$file_template" ).zip" + archive="$releasedir/$archive_name" + + nolib_archive_version="${project_version}-nolib" + nolib_archive_label="$( nolib=true filename_filter "$archive_label" )" + nolib_archive_name="$( nolib=true filename_filter "$file_template" ).zip" + # someone didn't include {nolib} and they're forcing nolib creation + if [ "$archive_label" = "$nolib_archive_label" ]; then + nolib_archive_label="${nolib_archive_label}-nolib" + fi + if [ "$archive_name" = "$nolib_archive_name" ]; then + nolib_archive_name="${nolib_archive_name%.zip}-nolib.zip" + fi + nolib_archive="$releasedir/$nolib_archive_name" + + if [ -n "$nolib" ]; then + archive_version="$nolib_archive_version" + archive_label="$nolib_archive_label" + archive_name="$nolib_archive_name" + archive="$nolib_archive" + nolib_archive= + fi + + if [[ -n $GITHUB_ACTIONS ]]; then + echo "archive_path=${archive}" >> "$GITHUB_OUTPUT" + fi + + start_group "Creating archive: $archive_name ($archive_label)" "archive" + if [ -f "$archive" ]; then + rm -f "$archive" + fi + ( cd "$releasedir" && zip -X -r "$archive" "${zip_root_dirs[@]}" ) + + if [ ! -f "$archive" ]; then + exit 1 + fi + end_group "archive" + + # Create nolib version of the zipfile + if [ -n "$enable_nolib_creation" ] && [ -z "$nolib" ] && [ -n "$nolib_exclude" ]; then + # run the nolib_filter + find "$pkgdir" -type f \( -name "*.xml" -o -name "*.toc" \) -print | while read -r file; do + case $file in + *.toc) _filter="toc_filter no-lib-strip true" ;; + *.xml) _filter="xml_filter no-lib-strip" ;; + esac + $_filter < "$file" > "$file.tmp" && mv "$file.tmp" "$file" + done + + # make the exclude paths relative to the release directory + nolib_exclude=${nolib_exclude//$releasedir\//} + + start_group "Creating no-lib archive: $nolib_archive_name ($nolib_archive_label)" "archive.nolib" + if [ -f "$nolib_archive" ]; then + rm -f "$nolib_archive" + fi + # shellcheck disable=SC2086 # set noglob so each nolib_exclude path gets quoted instead of expanded + ( set -f; cd "$releasedir" && zip -X -r -q "$nolib_archive" "${zip_root_dirs[@]}" -x $nolib_exclude ) + + if [ ! -f "$nolib_archive" ]; then + exit_code=1 + fi + end_group "archive.nolib" + fi +fi + +### +### Deploy the zipfile. +### + +# Upload to CurseForge. +upload_curseforge() { + if [[ -n "$skip_cf_upload" || -z "$slug" || -z "$cf_token" || -z "$project_site" ]]; then + return 0 + fi + + local _cf_game_version_id _cf_game_version _cf_versions + _cf_versions=$( curl -s -H "x-api-token: $cf_token" "$project_site/api/game/wow/versions" | jq -c '.' 2>/dev/null ) + if [[ -n $_cf_versions && $_cf_versions != *"errorMessage"* ]]; then + _cf_game_version_id= + _cf_game_version= + local version_id game_id invalid_version + for type in "${!game_type_version[@]}"; do + case $type in + classic) game_id=67408 ;; + bcc) game_id=73246 ;; + wrath) game_id=73713 ;; + cata) game_id=77522 ;; + mists) game_id=79434 ;; + titan) game_id=81212 ;; + forever) game_id=88568 ;; + *) game_id=517 + esac + IFS=':' read -ra V <<< "${game_type_version[$type]}" + for version in "${V[@]}"; do + # check the version + if ! jq -e --arg v "$version" --argjson t "$game_id" 'map(select(.gameVersionTypeID == $t and .name == $v)) | length > 0' <<< "$_cf_versions" &>/dev/null; then + invalid_version="$version" + # no match, so grab the next highest version (try to avoid testing versions) + version=$( echo "$_cf_versions" | jq -r --arg v "$invalid_version" --argjson t "$game_id" 'map(select(.gameVersionTypeID == $t and .name < $v)) | max_by(.id) | .name // empty' ) + if [[ -z $version ]]; then + # no? just grab the highest version + version=$( echo "$_cf_versions" | jq -r --argjson t "$game_id" 'map(select(.gameVersionTypeID == $t)) | max_by(.id) | .name // empty' ) + fi + if [[ -z $version ]]; then + echo "WARNING: No CurseForge game version match for \"$invalid_version\", ignoring" >&2 + else + echo "WARNING: No CurseForge game version match for \"$invalid_version\", using \"$version\"" >&2 + fi + fi + if [[ -n $version && "$_cf_game_version," != *",$version," ]]; then + _cf_game_version+=",${version}" + # get the id + version_id=$( echo "$_cf_versions" | jq -r --arg v "$version" --argjson t "$game_id" '.[] | select(.gameVersionTypeID == $t and .name == $v) | .id // empty' ) + _cf_game_version_id+=",${version_id}" + fi + done + done + _cf_game_version_id="[${_cf_game_version_id#,}]" + _cf_game_version=$( echo "${_cf_game_version#,}" | tr ',' '\n' | sort -Vr | tr '\n' ',' ) + _cf_game_version=${_cf_game_version%,} + fi + if [ -z "$_cf_game_version" ]; then + echo "Error fetching game version info from $project_site/api/game/versions" + echo + echo "Skipping upload to CurseForge." + echo + return 1 + fi + + local _cf_payload _cf_payload_relations + local resultfile result + local return_code=0 + + _cf_payload=$( cat <<-EOF + { + "displayName": "$archive_label", + "gameVersions": $_cf_game_version_id, + "releaseType": "$file_type", + "changelog": $( jq --slurp --raw-input '.' < "$changelog_path" ), + "changelogType": "$changelog_markup" + } + EOF + ) + _cf_payload_relations= + for i in "${!relations[@]}"; do + _cf_payload_relations="$_cf_payload_relations{\"slug\":\"$i\",\"type\":\"${relations[$i]}\"}," + done + if [[ -n $_cf_payload_relations ]]; then + _cf_payload_relations="{\"relations\":{\"projects\":[${_cf_payload_relations%,}]}}" + _cf_payload=$( echo "$_cf_payload $_cf_payload_relations" | jq -s -c '.[0] * .[1]' ) + fi + + echo "Uploading $archive_name ($_cf_game_version $file_type) to https://wow.curseforge.com/projects/$slug" + resultfile="$releasedir/cf_result.json" + if result=$( echo "$_cf_payload" | curl -sS --retry 3 --retry-delay 10 \ + -w "%{http_code}" -o "$resultfile" \ + -H "x-api-token: $cf_token" \ + -F "metadata=<-" \ + -F "file=@$archive" \ + "$project_site/api/projects/$slug/upload-file" + ); then + case $result in + 200) echo "Success!" ;; + 302) + echo "Error! ($result)" + # don't need to ouput the redirect page + return_code=1 + ;; + 404) + echo "Error! No project for \"$slug\" found." + return_code=1 + ;; + *) + echo "Error! ($result)" + if [ -s "$resultfile" ]; then + echo "$(<"$resultfile")" + fi + return_code=1 + ;; + esac + else + return_code=1 + fi + echo + + rm -f "$resultfile" 2>/dev/null + + return "$return_code" +} + +# Upload tags to WoWInterface. +upload_wowinterface() { + if [[ -z "$tag" || -z "$addonid" || -z "$wowi_token" ]]; then + return 0 + fi + + local _wowi_versions _wowi_game_version + _wowi_versions=$( curl -s https://api.wowinterface.com/addons/compatible.json | jq -c '.' 2>/dev/null ) + if [ -n "$_wowi_versions" ]; then + local wowi_type invalid_version + for type in "${!game_type_version[@]}"; do + case $type in + classic) wowi_type="Classic" ;; + bcc) wowi_type="TBC-Classic" ;; + wrath) wowi_type="WOTLK-Classic" ;; + cata|mists) wowi_type="Cata-Classic" ;; + titan|forever) + echo "WARNING: No WoWInterface game type match for \"$type\", \"$type\" is not supported, ignoring" >&2 + continue + ;; + *) wowi_type="Retail" + esac + IFS=':' read -ra V <<< "${game_type_version[$type]}" + for version in "${V[@]}"; do + # check the version + if ! jq -e --arg v "$version" 'map(select(.id == $v)) | length > 0' <<< "$_wowi_versions" &>/dev/null; then + invalid_version="$version" + # use the next highest version (try to avoid testing versions) + version=$( echo "$_wowi_versions" | jq -r --arg v "$invalid_version" --arg t "$wowi_type" 'map(select(.game == $t and .id < $v)) | max_by(.id) | .id // empty' ) + if [[ -z $version ]]; then + if [[ $type == "mists" ]]; then # XXX wowi dead, yo + wowi_type="Cata-Classic" + echo "WARNING: No WoWInterface game type match for \"$type\", using \"$wowi_type\"" >&2 + fi + # just grab the highest version + version=$( echo "$_wowi_versions" | jq -r --arg t "$wowi_type" 'map(select(.game == $t)) | max_by(.id) | .id // empty' ) + fi + if [[ -z $version ]]; then + echo "WARNING: No WoWInterface game version match for \"$invalid_version\", \"$wowi_type\" is not supported, ignoring" >&2 + else + echo "WARNING: No WoWInterface game version match for \"$invalid_version\", using \"$version\"" >&2 + fi + fi + if [[ -n $version && "$_wowi_game_version," != *",$version," ]]; then + _wowi_game_version+=",${version}" + fi + done + done + _wowi_game_version=$( echo "${_wowi_game_version#,}" | tr ',' '\n' | sort -Vr | tr '\n' ',' ) + _wowi_game_version=${_wowi_game_version%,} + fi + if [ -z "$_wowi_game_version" ]; then + echo "Error fetching game version info from https://api.wowinterface.com/addons/compatible.json" + echo + echo "Skipping upload to WoWInterface." + echo + return 1 + fi + + local -a _wowi_args + local resultfile result + local return_code=0 + + if [ -f "$wowi_changelog" ]; then + _wowi_args+=("-F changelog=<$wowi_changelog") + elif [ -n "$manual_changelog" ] || [ "$wowi_markup" = "markdown" ]; then + _wowi_args+=("-F changelog=<$changelog_path") + fi + if [ -z "$wowi_archive" ]; then + _wowi_args+=("-F archive=No") + fi + + echo "Uploading $archive_name ($_wowi_game_version) to https://www.wowinterface.com/downloads/info$addonid" + resultfile="$releasedir/wi_result.json" + if result=$( curl -sS --retry 3 --retry-delay 10 \ + -w "%{http_code}" -o "$resultfile" \ + -H "x-api-token: $wowi_token" \ + -F "id=$addonid" \ + -F "version=$archive_version" \ + -F "compatible=$_wowi_game_version" \ + "${_wowi_args[@]}" \ + -F "updatefile=@$archive" \ + "https://api.wowinterface.com/addons/update" + ); then + case $result in + 202) + echo "Success!" + if [ -f "$wowi_changelog" ]; then + rm -f "$wowi_changelog" 2>/dev/null + fi + ;; + 401) + echo "Error! No addon for id \"$addonid\" found or you do not have permission to upload files." + return_code=1 + ;; + 403) + echo "Error! Incorrect api key or you do not have permission to upload files." + return_code=1 + ;; + *) + echo "Error! ($result)" + if [ -s "$resultfile" ]; then + echo "$(<"$resultfile")" + fi + return_code=1 + ;; + esac + else + return_code=1 + fi + echo + + rm -f "$resultfile" 2>/dev/null + + return "$return_code" +} + +# Upload to Wago +upload_wago() { + if [[ -z "$wagoid" || -z "$wago_token" ]]; then + return 0 + fi + + local _wago_versions _wago_game_version _wago_support_property + _wago_versions=$( curl -s https://addons.wago.io/api/data/game | jq -c '.patches' 2>/dev/null ) + if [ -n "$_wago_versions" ]; then + _wago_support_property= + local wago_type wago_game_type_version invalid_version + for type in "${!game_type_version[@]}"; do + case $type in + bcc) wago_type="bc" ;; + wrath) wago_type="wotlk" ;; + mists) wago_type="mop" ;; + *) wago_type="$type" + esac + wago_game_type_version= + IFS=':' read -ra V <<< "${game_type_version[$type]}" + for version in "${V[@]}"; do + # check the version + if ! jq -e --arg t "$wago_type" --arg v "$version" '.[$t] // empty | index($v)' <<< "$_wago_versions" &>/dev/null; then + invalid_version="$version" + # no match, so grab the next highest version (try to avoid testing versions) + version=$( echo "$_wago_versions" | jq -r --arg t "$wago_type" --arg v "$invalid_version" '.[$t] // empty | map(select(. < $v)) | max // empty' ) + if [[ -z $version ]]; then + # just grab the highest version + version=$( echo "$_wago_versions" | jq -r --arg t "$wago_type" '.[$t] // empty | max // empty' ) + fi + if [[ -z $version ]]; then + echo "WARNING: No Wago game version match for \"$invalid_version\", ignoring" >&2 + else + echo "WARNING: No Wago game version match for \"$invalid_version\", using \"$version\"" >&2 + fi + fi + if [[ -n $version && "$_wago_game_version," != *",$version," ]]; then + wago_game_type_version+=",\"${version}\"" + _wago_game_version+=",${version}" + fi + done + if [[ -n $wago_game_type_version ]]; then + _wago_support_property+="\"supported_${wago_type}_patches\": [${wago_game_type_version#,}], " + fi + done + _wago_game_version=$( echo "${_wago_game_version#,}" | tr ',' '\n' | sort -Vr | tr '\n' ',' ) + _wago_game_version=${_wago_game_version%,} + fi + if [ -z "$_wago_support_property" ]; then + echo "Error fetching game version info from https://addons.wago.io/api/data/game" + echo + echo "Skipping upload to Wago." + echo + return 1 + fi + + local _wago_payload _wago_stability + local resultfile result + local return_code=0 + + _wago_stability="$file_type" + if [[ "$file_type" == "release" ]]; then + _wago_stability="stable" + fi + + _wago_payload=$( cat <<-EOF + { + "label": "$archive_label", + $_wago_support_property + "stability": "$_wago_stability", + "changelog": $( jq --slurp --raw-input '.' < "$changelog_path" ) + } + EOF + ) + + echo "Uploading $archive_name ($_wago_game_version $file_type) to https://addons.wago.io/addons/$wagoid" + resultfile="$releasedir/wago_result.json" + if result=$( echo "$_wago_payload" | curl -sS --retry 3 --retry-delay 10 \ + -w "%{http_code}" -o "$resultfile" \ + -H "authorization: Bearer $wago_token" \ + -H "accept: application/json" \ + -F "metadata=<-" \ + -F "file=@$archive" \ + "https://addons.wago.io/api/projects/$wagoid/version" + ); then + case $result in + 200|201) echo "Success!" ;; + 302) + echo "Error! ($result)" + # don't need to ouput the redirect page + return_code=1 + ;; + 403) + echo "Error! ($result)" + return_code=1 + ;; + 404) + echo "Error! No Wago project for id \"$wagoid\" found." + return_code=1 + ;; + *) + echo "Error! ($result)" + if [ -s "$resultfile" ]; then + echo "$(<"$resultfile")" + fi + return_code=1 + ;; + esac + else + return_code=1 + fi + echo + + rm -f "$resultfile" 2>/dev/null + + return "$return_code" +} + +# Create a GitHub Release for tags and upload the zipfile as an asset. +upload_github_asset() { + local asset_id result return_code=0 + local _ghf_release_id=$1 + local _ghf_file_name=$2 + local _ghf_file_path=$3 + local _ghf_resultfile="$releasedir/gh_asset_result.json" + local _ghf_content_type="application/${_ghf_file_name##*.}" # zip or json + + # check if an asset exists and delete it (editing a release) + asset_id=$( curl -sS \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $github_token" \ + "https://api.github.com/repos/$project_github_slug/releases/$_ghf_release_id/assets" \ + | jq --arg file "$_ghf_file_name" '.[] | select(.name? == $file) | .id' + ) + if [ -n "$asset_id" ]; then + curl -s \ + -X DELETE \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $github_token" \ + "https://api.github.com/repos/$project_github_slug/releases/assets/$asset_id" &>/dev/null + fi + + echo -n "Uploading $_ghf_file_name... " + if result=$( curl -sS --retry 3 --retry-delay 10 \ + -w "%{http_code}" -o "$_ghf_resultfile" \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $github_token" \ + -H "Content-Type: $_ghf_content_type" \ + --data-binary "@$_ghf_file_path" \ + "https://uploads.github.com/repos/$project_github_slug/releases/$_ghf_release_id/assets?name=$_ghf_file_name" + ); then + if [ "$result" = "201" ]; then + echo "Success!" + else + echo "Error ($result)" + if [ -s "$_ghf_resultfile" ]; then + echo "$(<"$_ghf_resultfile")" + fi + return_code=1 + fi + else + return_code=1 + fi + + rm -f "$_ghf_resultfile" 2>/dev/null + + return "$return_code" +} + +upload_github() { + if [[ -z "$tag" || -z "$project_github_slug" || -z "$github_token" ]]; then + return 0 + fi + + local _gh_metadata _gh_previous_metadata _gh_payload _gh_release_url _gh_method + local release_id versionfile resultfile result flavor title + local return_code=0 + + title=$( echo "$project" | jq --raw-input '.' ) + _gh_metadata='{ "name": '"$title"', "version": "'"$project_version"'", "filename": "'"$archive_name"'", "nolib": false, "metadata": [' + for type in "${!game_type_interface[@]}"; do + flavor="${game_flavor[$type]}" + [[ $flavor == "retail" ]] && flavor="mainline" + IFS=':' read -ra V <<< "${game_type_interface[$type]}" + for interface in "${V[@]}"; do + _gh_metadata+='{ "flavor": "'"${flavor}"'", "interface": '"${interface}"' },' + done + done + _gh_metadata=${_gh_metadata%,} + _gh_metadata+='] }' + if [ -f "$nolib_archive" ]; then + _gh_metadata+=',{ "name": '"$title"', "version": "'"$project_version"'", "filename": "'"$nolib_archive_name"'", "nolib": true, "metadata": [' + for type in "${!game_type_interface[@]}"; do + flavor="${game_flavor[$type]}" + [[ $flavor == "retail" ]] && flavor="mainline" + IFS=':' read -ra V <<< "${game_type_interface[$type]}" + for interface in "${V[@]}"; do + _gh_metadata+='{ "flavor": "'"${flavor}"'", "interface": '"${interface}"' },' + done + done + _gh_metadata=${_gh_metadata%,} + _gh_metadata+='] }' + fi + _gh_metadata='{ "releases": ['"$_gh_metadata"'] }' + + versionfile="$releasedir/release.json" + jq -c '.' <<< "$_gh_metadata" > "$versionfile" || echo "There was an error creating release.json" >&2 + + _gh_payload=$( cat <<-EOF + { + "tag_name": "$tag", + "name": "$tag", + "body": $( jq --slurp --raw-input '.' < "$changelog_path" ), + "draft": false, + "prerelease": $( [[ "$file_type" != "release" ]] && echo true || echo false ) + } + EOF + ) + resultfile="$releasedir/gh_result.json" + + release_id=$( curl -sS \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $github_token" \ + "https://api.github.com/repos/$project_github_slug/releases/tags/$tag" \ + | jq '.id // empty' + ) + if [ -n "$release_id" ]; then + echo "Updating GitHub release: https://github.com/$project_github_slug/releases/tag/$tag" + _gh_release_url="https://api.github.com/repos/$project_github_slug/releases/$release_id" + _gh_method="PATCH" + + # combine version info + _gh_metadata_url=$( curl -sS \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $github_token" \ + "https://api.github.com/repos/$project_github_slug/releases/$release_id/assets" \ + | jq -r '.[] | select(.name? == "release.json") | .url // empty' + ) + if [ -n "$_gh_metadata_url" ]; then + if _gh_previous_metadata=$( curl -sSL --fail \ + -H "Accept: application/octet-stream" \ + -H "Authorization: token $github_token" \ + "$_gh_metadata_url" + ); then + jq -sc '.[0].releases + .[1].releases | unique_by(.filename) | { releases: [.[]] }' <<< "${_gh_metadata} ${_gh_previous_metadata}" > "$versionfile" + else + echo "Warning: Unable to update release.json ($?)" + fi + fi + else + echo "Creating GitHub release: https://github.com/$project_github_slug/releases/tag/$tag" + _gh_release_url="https://api.github.com/repos/$project_github_slug/releases" + _gh_method="POST" + fi + if result=$( echo "$_gh_payload" | curl -sS --retry 3 --retry-delay 10 \ + -w "%{http_code}" -o "$resultfile" \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $github_token" \ + -X "$_gh_method" \ + -d @- \ + "$_gh_release_url" + ); then + if [ "$result" = "200" ] || [ "$result" = "201" ]; then # edited || created + if [ -z "$release_id" ]; then + release_id=$( jq '.id' < "$resultfile" ) + fi + upload_github_asset "$release_id" "$archive_name" "$archive" + if [ -f "$nolib_archive" ]; then + upload_github_asset "$release_id" "$nolib_archive_name" "$nolib_archive" + fi + if [ -s "$versionfile" ]; then + upload_github_asset "$release_id" "release.json" "$versionfile" + fi + else + echo "Error! ($result)" + if [ -s "$resultfile" ]; then + echo "$(<"$resultfile")" + fi + return_code=1 + fi + else + return_code=1 + fi + echo + + rm -f "$resultfile" 2>/dev/null + [ -z "$CI" ] && rm -f "$versionfile" 2>/dev/null + + return "$return_code" +} + + +if [[ -z $skip_upload && -n $archive && -s $archive ]]; then + if ! command -v jq &>/dev/null; then + echo "Skipping upload because \"jq\" was not found." + echo + exit_code=1 + else + if ! retry upload_curseforge; then + echo "Uploading to CurseForge failed, aborting uploads." >&2 + exit 1 + fi + upload_wowinterface || exit_code=1 + upload_wago || exit_code=1 + upload_github || exit_code=1 + fi +fi + +# All done. + +echo +echo "Packaging complete." +echo + +exit "$exit_code" diff --git a/.run/Release.run.xml b/.run/Release.run.xml new file mode 100644 index 0000000..442a07c --- /dev/null +++ b/.run/Release.run.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/Grichelde.toc b/Grichelde.toc index 04b2f50..fa51a5c 100644 --- a/Grichelde.toc +++ b/Grichelde.toc @@ -18,6 +18,7 @@ ## OptionalDeps: LibStub, CallbackHandler, Ace3, LibDataBroker, LibDBIcon ## SavedVariables: GricheldeDB +#@no-lib-strip@ Libs\LibStub\LibStub.lua Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml Libs\AceAddon-3.0\AceAddon-3.0.xml @@ -31,6 +32,7 @@ Libs\AceGUI-3.0\AceGUI-3.0.xml Libs\AceConfig-3.0\AceConfig-3.0.xml Libs\LibDataBroker-1.1\LibDataBroker-1.1.lua Libs\LibDBIcon-1.0\LibDBIcon-1.0.lua +#@end-no-lib-strip@ Grichelde.lua GricheldeConstants.lua @@ -43,6 +45,7 @@ GricheldeUpgrade.lua GricheldeOptions.lua GricheldeMinimap.lua GricheldeChat.lua + #@do-not-package@ GricheldeTest.lua -#@end-do-not-package@ +#@end-do-not-package@ \ No newline at end of file