Repository navigation
Expand file tree
/
Copy pathdev
More file actions
274 lines (234 loc) · 9.88 KB
/
Copy pathdev
File metadata and controls
274 lines (234 loc) · 9.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#!/bin/bash
set -euo pipefail
# Front door for build and release. DEVIATION from the global rule "build only via
# devcontainer": WPF needs Windows MSBuild, a Linux container cannot build
# net8.0-windows10.0.19041.0. So there is no container path here for this script to
# hide behind - it builds natively and only runs on Windows.
# Call from PowerShell/cmd via dev.cmd, from Git Bash directly: ./dev <command>
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
PROJECT="Battledeck/Battledeck.csproj"
TESTS="Battledeck.Tests/Battledeck.Tests.csproj"
VDPROJ="Setup/Setup.vdproj"
MANIFEST="Battledeck/app.manifest"
CHANGELOG="CHANGELOG.md"
RID="win-x64"
DIST="dist"
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) ;;
*) echo "ERROR: Windows only - WPF and net8.0-windows10.0.19041.0 do not build elsewhere." >&2
exit 1 ;;
esac
# --- Helfer -----------------------------------------------------------------
# PowerShell instead of Unix tools, because Git Bash ships neither zip nor uuidgen.
# -NoProfile: the user profile must not influence the build.
ps() { powershell -NoProfile -NonInteractive -Command "$1"; }
# Windows path for everything that goes to PowerShell - .NET does not understand /c/...
winpath() { cygpath -w "$1"; }
# The version sits in the csproj and is the source for everything else:
# the ZIP name, the release title, and the check against the tag in the workflow.
read_version() {
local v
v="$(grep -oE '<Version>[^<]+</Version>' "$PROJECT" | head -1 | sed -E 's|</?Version>||g')"
if [ -z "$v" ]; then
echo "ERROR: no <Version> in $PROJECT" >&2
exit 1
fi
echo "$v"
}
# Replaces exactly one occurrence and aborts otherwise. A silent "no match" would be
# expensive here: the version number would then read differently in three places.
replace_once() {
local file="$1" pattern="$2" replacement="$3" label="$4"
local count
count="$(grep -cE "$pattern" "$file" || true)"
if [ "$count" != "1" ]; then
echo "ERROR: $label matched $count times in $file, expected exactly 1" >&2
exit 1
fi
sed -i -E "s|$pattern|$replacement|" "$file"
}
new_guid() { ps '[guid]::NewGuid().ToString().ToUpper()' | tr -d '\r'; }
# --- Befehle ----------------------------------------------------------------
cmd_build() {
echo "==> Building (Debug)..."
dotnet build "$PROJECT" -c Debug
echo "==> Done"
}
cmd_test() {
echo "==> Testing..."
dotnet test "$TESTS" "$@"
echo "==> Done"
}
cmd_publish() {
local out="$DIST/publish"
echo "==> Publishing single-file $RID (Release)..."
rm -rf "$out"
# Framework-dependent, not self-contained: that is how the .exe has been shipped
# since 2.0.0 (34 MB instead of over 150). The price is that the .NET 8 desktop
# runtime has to be on the target machine - without an MSI nobody installs it
# afterward any more, that is what the README says.
dotnet publish "$PROJECT" \
-c Release \
-r "$RID" \
--self-contained false \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-o "$out"
echo "==> Done: $out/Battledeck.exe"
}
cmd_release() {
local version stage zip
version="$(read_version)"
stage="$DIST/stage"
zip="$DIST/Battledeck_${version}_${RID}.zip"
cmd_publish
echo "==> Staging $version..."
rm -rf "$stage"
mkdir -p "$stage"
# Copy deliberately instead of zipping the publish folder: the .pdb sits there
# too, and it does not belong in a release.
cp "$DIST/publish/Battledeck.exe" "$stage/"
# One-time bridge for anyone still on 1.8.0 or earlier: their UpdateInstaller was compiled
# with ExeName = "Smurftown.exe", and that constant sits in an already-shipped binary - it
# cannot be patched after the fact. Without a file by that name in this ZIP, Extract() on
# those clients throws "the package holds no Smurftown.exe" the moment they try to update
# past 1.8.0, even though the download and its checksum are fine. A second, byte-identical
# copy under the old name costs nothing at runtime: Swap() replaces the path of the running
# process, not a fixed file name, so it does not care what the ZIP called the file it just
# extracted. Delete this line once this bridge release has shipped - every client that
# received it is then running code that looks for Battledeck.exe like everyone else. See
# docs/self-update.md.
cp "$DIST/publish/Battledeck.exe" "$stage/Smurftown.exe"
cp README.md "$stage/"
echo "==> Zipping..."
rm -f "$zip"
ps "Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory('$(winpath "$stage")', '$(winpath "$PWD/$zip")', [System.IO.Compression.CompressionLevel]::Optimal, \$false)"
rm -rf "$stage"
echo "==> Checksums..."
( cd "$DIST" && sha256sum "$(basename "$zip")" > checksums.txt && cat checksums.txt )
echo "==> Done:"
ls -lh "$DIST"/*.zip "$DIST"/checksums.txt
}
cmd_version() {
if [ $# -eq 0 ]; then
read_version
return
fi
local version="$1"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "ERROR: expected x.y.z, got '$version'" >&2
exit 1
fi
echo "==> Setting version $version"
replace_once "$PROJECT" \
'<Version>[^<]+</Version>' \
"<Version>${version}</Version>" \
"csproj <Version>"
echo " $PROJECT <Version>$version</Version>"
replace_once "$MANIFEST" \
'(assemblyIdentity version=")[^"]+(")' \
"\\1${version}.0\\2" \
"manifest assemblyIdentity"
echo " $MANIFEST version=\"${version}.0\""
replace_once "$VDPROJ" \
'("ProductVersion" = "8:)[^"]*"' \
"\\1${version}\"" \
"vdproj ProductVersion"
echo " $VDPROJ ProductVersion $version"
# ProductCode and PackageCode need to be regenerated on every version change,
# otherwise RemovePreviousVersions and DetectNewerInstalledVersion do not kick
# in. The UpgradeCode stays untouched - it is what the installer uses to
# recognize the previous version.
local pc kc
pc="$(new_guid)"
kc="$(new_guid)"
replace_once "$VDPROJ" \
'("ProductCode" = "8:)\{[^}]*\}"' \
"\\1{${pc}}\"" \
"vdproj ProductCode"
replace_once "$VDPROJ" \
'("PackageCode" = "8:)\{[^}]*\}"' \
"\\1{${kc}}\"" \
"vdproj PackageCode"
echo " $VDPROJ ProductCode {$pc}"
echo " $VDPROJ PackageCode {$kc}"
echo " $VDPROJ UpgradeCode unchanged"
echo "==> Done. Commit the change, then tag '$version' and push the tag to release."
}
# Cuts exactly one section out of the changelog and prints its body. The release
# workflow hands that to the GitHub release, and an installed copy that cannot update
# itself opens exactly that page - so an empty section is not a cosmetic flaw but a
# release nobody can read. Hence abort instead of an empty body, the same reasoning as
# in replace_once.
cmd_notes() {
local version="${1:-}"
[ -n "$version" ] || version="$(read_version)"
if [ ! -f "$CHANGELOG" ]; then
echo "ERROR: $CHANGELOG does not exist" >&2
exit 1
fi
# -F: the heading carries brackets, and those are regex metacharacters.
if ! grep -qF "## [$version]" "$CHANGELOG"; then
echo "ERROR: no section '## [$version]' in $CHANGELOG - rename the [Upcoming] heading first" >&2
exit 1
fi
local body
# Ends at the next heading OR at the first link definition at the bottom of the
# file - without the second condition the last section swallows them.
body="$(awk -v v="$version" '
index($0, "## [" v "]") == 1 { inside = 1; next }
inside && (/^## / || /^\[[^]]+\]: /) { exit }
inside { print }
' "$CHANGELOG" | sed '/./,$!d')"
if [ -z "$body" ]; then
echo "ERROR: section '## [$version]' in $CHANGELOG is empty" >&2
exit 1
fi
printf '%s
' "$body"
}
cmd_clean() {
echo "==> Cleaning..."
rm -rf "$DIST"
dotnet clean "$PROJECT" -c Debug >/dev/null 2>&1 || true
dotnet clean "$PROJECT" -c Release >/dev/null 2>&1 || true
echo "==> Done"
}
cmd_help() {
cat <<'HELP'
Usage: ./dev <command> [args...] (from PowerShell/cmd: .\dev.cmd <command>)
Commands:
build dotnet build -c Debug
test [args...] dotnet test - extra args go to dotnet test, e.g. --filter
publish Single-file Release publish for win-x64 into dist/publish/
release publish + dist/Battledeck_<version>_win-x64.zip + checksums.txt
version Print the version from Battledeck.csproj
version <x.y.z> Set it in csproj, app.manifest and Setup.vdproj
(fresh ProductCode + PackageCode, UpgradeCode untouched)
notes [x.y.z] Print the CHANGELOG.md section of that version (default: the
current one) - this is what the release body is built from
clean Remove dist/ and the dotnet build outputs
help This text
Releasing:
rename '## [Upcoming]' in CHANGELOG.md to '## [2.0.1] - <date>'
./dev version 2.0.1 bump the three files
./dev notes 2.0.1 check what the release body will say
commit, then tag 2.0.1 and push the tag
the tag starts .github/workflows/release.yml, which runs
'./dev release' on windows-latest and attaches the assets
Windows only - there is no container path for WPF, so this script builds natively.
Starting the app is not a command here: that stays a user step.
HELP
}
case "${1:-help}" in
build) cmd_build ;;
test) shift; cmd_test "$@" ;;
publish) cmd_publish ;;
release) cmd_release ;;
version) shift; cmd_version "$@" ;;
notes) shift; cmd_notes "$@" ;;
clean) cmd_clean ;;
help) cmd_help ;;
*) echo "Unknown command: $1" >&2; cmd_help; exit 1 ;;
esac