summaryrefslogtreecommitdiffstats
path: root/windows/cross_build_mxe.sh
blob: 23e1b6e070af238737710c82729dcd653eccf26f (plain) (blame)
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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
#!/bin/bash

# Build script that cross-builds Pica Pica Messenger
# for Windows on a GNU/Linux or Mac host using MXE build environment

# make sure that MXE dependencies are installed on a host system first
# see https://mxe.cc for details

# The MXE tree (clone, patches, libraries) is set up only once and reused on
# every subsequent run, because building the MXE libraries takes hours.
# Pica Pica itself and the installers are always rebuilt from scratch.

# Environment variables understood by this script:
#   MXE_COMMIT                  build against another MXE commit than the pinned one
#   MXE_ALLOW_COMMIT_MISMATCH=1 only warn instead of failing when the existing MXE
#                               clone is checked out at a different commit
#   MXE_FORCE_REAPPLY_PATCHES=1 restore the MXE files touched by our patches from
#                               git and apply the patches again, in case a patch was
#                               changed after it had been applied

set -o errexit
set -o errtrace
set -o nounset
set -o pipefail

MXE_REPO_URL='https://github.com/mxe/mxe.git'
MXE_COMMIT=${MXE_COMMIT:-4efbb3567463a6d0a4d8ad7b822af0881bb1d9f8}
MXE_TARGETS='i686-w64-mingw32.shared x86_64-w64-mingw32.shared'
MXE_PACKAGES='cc qtbase ffmpeg openssl miniupnpc libevent sqlite webrtc-audio-processing nsis'
MXE_PATCHES='mxe_qtbase_strip_excess_deps.patch mxe_ffmpeg_strip_excess_deps.patch'

windows_dir=$(cd "$(dirname "$0")" && pwd)
mxe_dir=$windows_dir/mxe

function die {
	echo "ERROR: $*" >&2
	exit 1
}

function on_error {
	echo "ERROR: $0 failed at line $1 (exit code $2), build aborted" >&2
}

trap 'on_error $LINENO $?' ERR

# step 1: clone the MXE repository unless it is already there
function clone_mxe {
	if test -d "$mxe_dir/.git"
	then
		echo "*** MXE is already cloned in $mxe_dir, skipping clone"
		return
	fi

	test -e "$mxe_dir" && die "$mxe_dir exists but is not a git repository, remove it and run this script again"

	echo "*** cloning MXE from $MXE_REPO_URL"
	git clone "$MXE_REPO_URL" "$mxe_dir"
}

# step 2: make sure the MXE clone is checked out at the commit we build against
function checkout_mxe_commit {
	local head

	head=$(git -C "$mxe_dir" rev-parse HEAD)

	if test "$head" = "$MXE_COMMIT"
	then
		echo "*** MXE is already checked out at $MXE_COMMIT, skipping checkout"
		return
	fi

	if test "${MXE_ALLOW_COMMIT_MISMATCH:-0}" = '1'
	then
		echo "*** WARNING: MXE is checked out at $head instead of $MXE_COMMIT, continuing anyway" >&2
		return
	fi

	if ! git -C "$mxe_dir" cat-file -e "$MXE_COMMIT^{commit}" 2>/dev/null
	then
		echo "*** commit $MXE_COMMIT is not present in the local MXE clone, fetching"
		git -C "$mxe_dir" fetch origin "$MXE_COMMIT" || git -C "$mxe_dir" fetch origin
		git -C "$mxe_dir" cat-file -e "$MXE_COMMIT^{commit}" 2>/dev/null \
			|| die "commit $MXE_COMMIT does not exist in $MXE_REPO_URL, fix MXE_COMMIT in $0"
	fi

	if test -n "$(git -C "$mxe_dir" status --porcelain --untracked-files=no)"
	then
		die "$mxe_dir is checked out at $head instead of $MXE_COMMIT and has local modifications.
Checking out $MXE_COMMIT would discard them, so it is not done automatically. Either
  - check out $MXE_COMMIT in $mxe_dir manually, or
  - set MXE_COMMIT to the commit you want to build against, or
  - run with MXE_ALLOW_COMMIT_MISMATCH=1 to keep the current checkout, or
  - remove $mxe_dir to get a fresh clone (this rebuilds all MXE libraries)"
	fi

	echo "*** checking out MXE commit $MXE_COMMIT"
	git -C "$mxe_dir" checkout --detach "$MXE_COMMIT"
}

# files a patch touches, as paths relative to the MXE tree
function patch_target_files {
	awk '/^\+\+\+ /{sub(/^\+\+\+ /, ""); sub(/\t.*$/, ""); sub(/^b\//, ""); print}' "$1"
}

# step 3: apply our MXE patches, but only those that are not applied yet
function apply_mxe_patches {
	local patch_name patch_file

	for patch_name in $MXE_PATCHES
	do
		patch_file=$windows_dir/$patch_name

		test -f "$patch_file" || die "patch $patch_file not found"

		if patch -p1 -d "$mxe_dir" --dry-run --reverse --silent < "$patch_file" > /dev/null 2>&1
		then
			echo "*** $patch_name is already applied, skipping"
			continue
		fi

		if patch -p1 -d "$mxe_dir" --dry-run --forward --silent < "$patch_file" > /dev/null 2>&1
		then
			echo "*** applying $patch_name"
			patch -p1 -d "$mxe_dir" --forward < "$patch_file"
			continue
		fi

		if test "${MXE_FORCE_REAPPLY_PATCHES:-0}" = '1'
		then
			echo "*** restoring $(patch_target_files "$patch_file" | tr '\n' ' ')from git and applying $patch_name again"
			# shellcheck disable=SC2046
			git -C "$mxe_dir" checkout -- $(patch_target_files "$patch_file")
			patch -p1 -d "$mxe_dir" --forward < "$patch_file"
			continue
		fi

		die "$patch_name neither applies to nor is already applied in $mxe_dir.
This usually means the patch was changed after it had been applied. Run
  patch -p1 -d $mxe_dir --dry-run --forward < $patch_file
to see the details, then either resolve it by hand or run this script with
MXE_FORCE_REAPPLY_PATCHES=1 to restore the patched MXE files from git and
apply the patch again (the affected libraries will be rebuilt by MXE)."
	done
}

# MXE packages from $MXE_PACKAGES that are not built yet, as 'package (target)'
function missing_mxe_packages {
	local target package

	for target in $MXE_TARGETS
	do
		for package in $MXE_PACKAGES
		do
			test -f "$mxe_dir/usr/$target/installed/$package" || echo "$package ($target)"
		done
	done
}

# step 4: build the libraries provided by MXE, unless they are all built already
function build_mxe_packages {
	local missing

	make -C "$mxe_dir" MXE_TARGETS="$MXE_TARGETS" check-requirements

	missing=$(missing_mxe_packages)

	if test -z "$missing"
	then
		echo "*** all MXE packages are already built, skipping MXE build"
		return
	fi

	echo "*** MXE packages that are not built yet:"
	echo "$missing" | sed 's,^,      ,'

	# TODO: libva for windows, enable vaapi in ffmpeg configuration
	# make itself does nothing for the packages that are built already
	make -C "$mxe_dir" MXE_TARGETS="$MXE_TARGETS" $MXE_PACKAGES

	missing=$(missing_mxe_packages)
	test -z "$missing" || die "MXE build finished but these packages are still missing:
$(echo "$missing" | sed 's,^,      ,')"
}

# makensis for the given architecture.
#
# makensis is a program for the host system, but MXE installs it into the
# target prefix - usr/<target>/bin - because the installer stubs it carries are
# built for that target: the copy from the x86_64-w64-mingw32.shared tree emits
# 64 bit installers, the one from the i686-w64-mingw32.shared tree 32 bit ones,
# and a 64 bit installer does not run on a 32 bit Windows. So the copy
# belonging to the architecture being packaged is used when it is there, any
# other MXE built copy after that, then usr/bin/makensis for an nsis built into
# the MXE tree by hand, and finally whatever the host system has.
#
# usr/<target>/bin is deliberately not added to PATH: it is full of Windows
# executables, and configure --host=<target> looks for host programs with the
# .exe suffix as well, so a Windows uic.exe or sqlite3.exe in there could be
# picked up as if it were a host tool.
function find_makensis {
	local wanted_arch=$1
	local other_arch candidate

	for candidate in "$mxe_dir/usr/$wanted_arch/bin/makensis" \
	                 $(for other_arch in $MXE_TARGETS; do echo "$mxe_dir/usr/$other_arch/bin/makensis"; done) \
	                 "$mxe_dir/usr/bin/makensis"
	do
		if test -x "$candidate"
		then
			echo "$candidate"
			return
		fi
	done

	command -v makensis || die "makensis not found: no nsis in $mxe_dir/usr/*/bin, none on the host system"
}

# step 5: build Pica Pica itself and the installer, always from scratch
function build_for_arch {

arch=$1

PREFIX=$PWD/usr/$arch

test -x ../../configure || die "../../configure not found, run ./autogen.sh in the Pica Pica source tree first"

rm -rf build_pica-pica
mkdir -p build_pica-pica
cd build_pica-pica
../../../configure --host=$arch --prefix=$PREFIX
make -j$(nproc)
make install
make clean
cd ..

#copy exe & DLLs
rm -rf build_installer_$arch
mkdir -p build_installer_$arch
cp $PREFIX/bin/pica-client.exe build_installer_$arch/
cp $PREFIX/bin/pica-node.exe build_installer_$arch/

#run copydlldeps.sh from destination dir, otherwise it recursively walks all MXE tree and copies excess DLLs

cd build_installer_$arch
#the styles directory is what keeps the program from looking like Windows 95:
#Qt's themed look on Windows lives in the windowsvista style, which is a
#plugin, and without it QApplication falls back to the built-in Windows style
../tools/copydlldeps.sh --infile ./pica-client.exe --destdir ./ --recursivesrcdir $PREFIX --copy --enforcedir $PREFIX/qt5/plugins/platforms/ --enforcedir $PREFIX/qt5/plugins/sqldrivers/ --enforcedir $PREFIX/qt5/plugins/imageformats/ --enforcedir $PREFIX/qt5/plugins/styles/  --objdump ../usr/bin/$arch-objdump

../tools/copydlldeps.sh --infile ./pica-node.exe --destdir ./ --recursivesrcdir $PREFIX --copy  --objdump ../usr/bin/$arch-objdump

cd ..

#strip everything that was copied, including the Qt plugin subdirectories.
#find is used instead of globs because a glob for a plugin directory that
#copydlldeps.sh did not create would abort the whole script now that it runs
#with errexit - makensis reports a missing plugin later anyway.
find build_installer_$arch -type f \( -name '*.dll' -o -name '*.exe' \) -print0 \
	| xargs -0 -r usr/bin/$arch-strip

#build installer
cd build_installer_$arch

for dll in *.dll
do
	echo "File $dll" >> dll_list.nsh
done

cp ../../../COPYING ./
cp ../../../README ./
cp ../../../windows/picapica.ico ./

mkdir -p share

cp $PREFIX/share/pica-client/dhparam4096.pem share/
cp $PREFIX/share/pica-client/picapica-icon-fly.png share/
cp $PREFIX/share/pica-client/picapica-icon-sit.png share/
cp $PREFIX/share/pica-client/picapica-snd-newmessage.wav share/

makensis_bin=$(find_makensis $arch)
echo "*** building the installer with $makensis_bin"

if test $arch = 'i686-w64-mingw32.shared'
then
	cp ../build_pica-pica/windows/installer32.nsi ./
	"$makensis_bin" installer32.nsi
elif test $arch = 'x86_64-w64-mingw32.shared'
then
	cp ../build_pica-pica/windows/installer64.nsi ./
	"$makensis_bin" installer64.nsi
else
	die "unsupported architecture $arch"
fi

cd ..
}


clone_mxe
checkout_mxe_commit
apply_mxe_patches
build_mxe_packages

cd "$mxe_dir"

export PATH=$PWD/usr/bin/:$PATH

# resolve makensis for every architecture now, so that a missing nsis is
# reported before the hour the Pica Pica builds take, not after them
for arch in $MXE_TARGETS
do
	find_makensis $arch > /dev/null
done

for arch in $MXE_TARGETS
do
	build_for_arch $arch
done

echo "*** done, installers are in:"
for arch in $MXE_TARGETS
do
	echo "      $mxe_dir/build_installer_$arch"
done