1+ #! /usr/bin/env bash
2+ # ─────────────────────────────────────────────────────────────────────
3+ # Unified FPC + Lazarus installer for CI.
4+ #
5+ # Fetches the FPC tarball from the official freepascal.org mirror,
6+ # runs install.sh non-interactively, then clones Lazarus and builds
7+ # lazbuild. Works on Linux, macOS, *BSD, Solaris, and Windows
8+ # (the latter via Git Bash, which ships pre-installed on
9+ # windows-latest GitHub runners).
10+ #
11+ # All work happens under $HOME — no sudo, no system-level changes.
12+ #
13+ # Inputs (env vars):
14+ # FPC_TARGET e.g. x86_64-linux, aarch64-darwin, x86_64-win64
15+ # FPC_VERSION FPC release version, e.g. 3.2.2
16+ # INSTALL_PREFIX where FPC is installed (default: $HOME/fpc-install)
17+ # LAZARUS_DIR where Lazarus source is cloned and built
18+ # (default: $HOME/lazarus-src)
19+ # LAZARUS_BRANCH branch/tag to clone, e.g. lazarus_4_4
20+ # LAZARUS_REPO git URL
21+ # MAKE_CMD 'make' on Linux/Windows/macOS, 'gmake' on BSD/Solaris
22+ # (auto-detected if unset)
23+ #
24+ # Outputs (appended to $GITHUB_PATH if set):
25+ # $INSTALL_PREFIX/bin and $LAZARUS_DIR are added to PATH
26+ # ─────────────────────────────────────────────────────────────────────
27+
28+ set -xeuo pipefail
29+
30+ : " ${FPC_VERSION:? FPC_VERSION is required (e.g. 3.2.2)} "
31+ : " ${FPC_TARGET:? FPC_TARGET is required (e.g. x86_64-linux)} "
32+ : " ${LAZARUS_BRANCH:? LAZARUS_BRANCH is required} "
33+ : " ${LAZARUS_REPO:? LAZARUS_REPO is required} "
34+
35+ case " $( uname -s) " in
36+ MINGW* |MSYS* |CYGWIN* ) IS_WINDOWS=1 ;;
37+ * ) IS_WINDOWS=0 ;;
38+ esac
39+
40+ : " ${INSTALL_PREFIX:= $HOME / fpc-install} "
41+ : " ${LAZARUS_DIR:= $HOME / lazarus-src} "
42+
43+ # Pick GNU make. On most platforms 'make' is GNU make. On BSDs and
44+ # Solaris, 'make' is BSD make and we need 'gmake'. On Windows
45+ # (windows-latest runner) the only GNU make pre-installed is
46+ # Strawberry Perl's 'gmake.exe' — there is no 'make' on PATH.
47+ # Caller can override MAKE_CMD if they have a different setup.
48+ if [ -z " ${MAKE_CMD:- } " ]; then
49+ case " $( uname -s) " in
50+ * BSD|DragonFly|SunOS) MAKE_CMD=" gmake" ;;
51+ MINGW* |MSYS* |CYGWIN* ) MAKE_CMD=" gmake" ;;
52+ * ) MAKE_CMD=" make" ;;
53+ esac
54+ fi
55+
56+ # ── Fetch + extract ──────────────────────────────────────────────────
57+
58+ # Some BSD tarballs have an OS-version suffix in the filename, e.g.
59+ # fpc-3.2.2.x86_64-freebsd11.tar. The tarball extracts to a directory
60+ # WITHOUT the version suffix. Map our canonical FPC_TARGET to the
61+ # actual filename here; let the post-extract glob handle the directory
62+ # name.
63+ case " $FPC_TARGET " in
64+ x86_64-freebsd) TAR_TARGET=" x86_64-freebsd11" ;;
65+ * ) TAR_TARGET=" $FPC_TARGET " ;;
66+ esac
67+
68+ TARBALL=" fpc-${FPC_VERSION} .${TAR_TARGET} .tar"
69+ URL=" http://downloads.freepascal.org/fpc/dist/${FPC_VERSION} /${FPC_TARGET} /${TARBALL} "
70+
71+ WORK_DIR=" $( mktemp -d 2> /dev/null || mktemp -d -t fpc-install) "
72+ cd " $WORK_DIR "
73+
74+ echo " Downloading $URL "
75+ # curl is on every platform we target; wget isn't (macOS notably).
76+ # Retry to ride out transient mirror hiccups.
77+ curl -fL --retry 5 --retry-delay 5 --retry-all-errors \
78+ -o " $TARBALL " " $URL "
79+
80+ tar xf " $TARBALL "
81+
82+ # Glob the extracted directory: name varies (freebsd11 → freebsd).
83+ # shellcheck disable=SC2086
84+ set -- " fpc-${FPC_VERSION} " .*
85+ EXTRACT_DIR=" $1 "
86+ cd " $EXTRACT_DIR "
87+
88+ # ── Run install.sh non-interactively ─────────────────────────────────
89+ #
90+ # The script asks (in order, each conditional on a file being present):
91+ # 1. Install prefix [always]
92+ # 2. Install Cross binutils? (Y/n) [if binutils-*.tar.gz]
93+ # 3. Install Textmode IDE? (Y/n) [if ide.<target>.tar.gz]
94+ # 4. Install documentation? (Y/n) [if doc-pdf.tar.gz]
95+ # 5. Install demos? (Y/n) [if demo.tar.gz]
96+ # 6. Install demos in <dir> [path] [only if 5 was Y]
97+ # 7. Substitute version by $fpcversion? (Y/n) [if fpc.cfg has version]
98+ #
99+ # We answer the prefix explicitly, then 'n' to everything optional.
100+ # install.sh's yesno() treats empty input as YES, so if our answer
101+ # stream runs short we'd silently install bundles we don't want.
102+ # We send 10 'n's (more than the max number of yesno prompts) to
103+ # guarantee every prompt gets an explicit 'n'. A bounded list is
104+ # preferable to `yes n` — the latter receives SIGPIPE on consumer
105+ # exit, which `set -o pipefail` would surface as a pipeline failure.
106+ mkdir -p " $INSTALL_PREFIX "
107+
108+ # install.sh doesn't `set -e`, so it returns 0 even when its final
109+ # samplecfg call fails (which it does on Windows — see below). We
110+ # verify the install succeeded by checking for the compiler binary.
111+ printf ' %s\nn\nn\nn\nn\nn\nn\nn\nn\nn\nn\n' " $INSTALL_PREFIX " \
112+ | bash ./install.sh
113+
114+ # ── Locate the installed compiler ────────────────────────────────────
115+ #
116+ # Layout differs between Unix and Windows:
117+ # Unix: $PREFIX/bin/fpc (single binary)
118+ # Windows: $PREFIX/bin/<target>/fpc.exe (target-specific)
119+ # $PREFIX/bin/instantfpc.exe (target-independent
120+ # utilities)
121+ # Both directories need to be on PATH on Windows.
122+ if [ " $IS_WINDOWS " = " 1" ]; then
123+ FPC_EXE=" $INSTALL_PREFIX /bin/$FPC_TARGET /fpc.exe"
124+ FPC_UTIL_DIR=" $INSTALL_PREFIX /bin"
125+ else
126+ FPC_EXE=" $INSTALL_PREFIX /bin/fpc"
127+ FPC_UTIL_DIR=" "
128+ fi
129+
130+ if [ ! -f " $FPC_EXE " ]; then
131+ echo " ERROR: FPC compiler not found at $FPC_EXE " >&2
132+ ls -la " $INSTALL_PREFIX /bin/" || true
133+ if [ " $IS_WINDOWS " = " 1" ]; then
134+ ls -la " $INSTALL_PREFIX /bin/$FPC_TARGET /" || true
135+ fi
136+ exit 1
137+ fi
138+
139+ FPC_BIN_DIR=" $( dirname " $FPC_EXE " ) "
140+ if [ -n " $FPC_UTIL_DIR " ]; then
141+ export PATH=" $FPC_BIN_DIR :$FPC_UTIL_DIR :$PATH "
142+ else
143+ export PATH=" $FPC_BIN_DIR :$PATH "
144+ fi
145+
146+ # ── Linux glibc 2.34+ workaround ─────────────────────────────────────
147+ #
148+ # FPC 3.2.2 was built against glibc < 2.34 and its cprt0.o references
149+ # __libc_csu_init / __libc_csu_fini, which glibc 2.34 (Aug 2021) made
150+ # private. Linking anything FPC produces against current glibc fails:
151+ # undefined reference to `__libc_csu_init'
152+ # undefined reference to `__libc_csu_fini'
153+ #
154+ # Ubuntu 22.04+ ships glibc 2.34+ and is affected. The fix shipped in
155+ # FPC 3.2.4+ (and Debian/Fedora patched their packages); for our
156+ # pre-built tarball we patch in place by merging stub object code
157+ # into cprt0.o, satisfying the symbols at link time.
158+ #
159+ # Affected: every Linux target (x86_64, aarch64, arm/armhf). The
160+ # 'cprt0' name is consistent across architectures.
161+ #
162+ # See https://gitlab.com/freepascal.org/fpc/source/-/issues/39295
163+ if [ " $( uname -s) " = " Linux" ]; then
164+ RTL_DIR=" $INSTALL_PREFIX /lib/fpc/$FPC_VERSION /units/$FPC_TARGET /rtl"
165+ CPRT0=" $RTL_DIR /cprt0.o"
166+ if [ -f " $CPRT0 " ]; then
167+ STUB_C=" $WORK_DIR /csu_stubs.c"
168+ STUB_O=" $WORK_DIR /csu_stubs.o"
169+ cat > " $STUB_C " << 'EOF '
170+ /* glibc 2.34+ removed __libc_csu_init / __libc_csu_fini. FPC 3.2.2's
171+ cprt0.o still references them. Provide empty stubs so the linker
172+ is satisfied. */
173+ void __libc_csu_init(int argc, char **argv, char **envp) { (void)argc; (void)argv; (void)envp; }
174+ void __libc_csu_fini(void) {}
175+ EOF
176+ cc -c -fPIC -o " $STUB_O " " $STUB_C "
177+ # Merge stubs into cprt0.o using `ld -r` (relocatable link).
178+ # cprt0.o references __libc_csu_*; the stub provides them; the
179+ # merged object resolves both within itself.
180+ ld -r -o " $CPRT0 .new" " $CPRT0 " " $STUB_O "
181+ mv " $CPRT0 .new" " $CPRT0 "
182+ echo " Patched $CPRT0 with glibc 2.34+ stubs."
183+ fi
184+ fi
185+
186+ # ── Windows-specific: generate fpc.cfg ───────────────────────────────
187+ #
188+ # install.sh's final step calls $LIBDIR/samplecfg, which is a POSIX
189+ # shell script that does not ship in the Windows tarball. The Windows
190+ # replacement is fpcmkcfg.exe at $PREFIX/bin/. Without an fpc.cfg,
191+ # the compiler can't find its own units. Note: fpc.cfg goes next to
192+ # fpc.exe in $PREFIX/bin/<target>/, since FPC searches there first.
193+ if [ " $IS_WINDOWS " = " 1" ]; then
194+ FPCMKCFG=" $FPC_UTIL_DIR /fpcmkcfg.exe"
195+ if [ ! -f " $FPCMKCFG " ]; then
196+ echo " ERROR: fpcmkcfg.exe not found at $FPCMKCFG " >&2
197+ exit 1
198+ fi
199+ " $FPCMKCFG " -d " basepath=$INSTALL_PREFIX /lib/fpc/$FPC_VERSION " \
200+ -o " $FPC_BIN_DIR /fpc.cfg"
201+ fi
202+
203+ # Add to GitHub Actions PATH so subsequent steps see fpc.
204+ # On Windows, GITHUB_PATH expects native Windows paths (C:\foo\bin),
205+ # not MSYS/Git Bash paths (/c/foo/bin). cygpath converts both ways.
206+ # Both bin/<target> and bin/ go on PATH so subsequent steps find both
207+ # fpc.exe (the compiler) and instantfpc.exe (the utility).
208+ if [ -n " ${GITHUB_PATH:- } " ]; then
209+ if [ " $IS_WINDOWS " = " 1" ]; then
210+ cygpath -w " $FPC_BIN_DIR " >> " $GITHUB_PATH "
211+ cygpath -w " $FPC_UTIL_DIR " >> " $GITHUB_PATH "
212+ else
213+ echo " $FPC_BIN_DIR " >> " $GITHUB_PATH "
214+ fi
215+ fi
216+
217+ fpc -iV
218+ fpc -iSO
219+ fpc -iSP
220+
221+ # ── Build Lazarus from source ────────────────────────────────────────
222+ #
223+ # We always build lazbuild from source rather than using a packaged
224+ # Lazarus. Reasons:
225+ # - Cross-platform consistency: same code path on all 10 targets.
226+ # - The packaged Lazarus on Linux/Windows pulls in the full IDE
227+ # (GTK / Qt / native widgets), which we don't need.
228+ # - lazbuild builds in ~1–2 minutes; cheap relative to the FPC fetch.
229+
230+ git clone --depth 1 --branch " $LAZARUS_BRANCH " " $LAZARUS_REPO " " $LAZARUS_DIR "
231+
232+ # DragonFlyBSD: Lazarus is missing include/dragonfly/lazconf.inc.
233+ # DragonFlyBSD is a FreeBSD derivative, so the FreeBSD include works
234+ # as-is. Patch it in before building.
235+ if [ " $( uname -s) " = " DragonFly" ]; then
236+ DF_INC=" $LAZARUS_DIR /ide/packages/ideconfig/include/dragonfly"
237+ if [ ! -f " $DF_INC /lazconf.inc" ]; then
238+ mkdir -p " $DF_INC "
239+ cp " $LAZARUS_DIR /ide/packages/ideconfig/include/freebsd/lazconf.inc" \
240+ " $DF_INC /lazconf.inc"
241+ fi
242+ fi
243+
244+ # On Windows, gmake.exe is a native PE binary that may not understand
245+ # Git Bash's /c/Users/... path style. Pass a Windows-format path.
246+ if [ " $IS_WINDOWS " = " 1" ]; then
247+ $MAKE_CMD -C " $( cygpath -w " $LAZARUS_DIR " ) " lazbuild
248+ else
249+ $MAKE_CMD -C " $LAZARUS_DIR " lazbuild
250+ fi
251+
252+ # ── Write Lazarus environmentoptions.xml ─────────────────────────────
253+ # lazbuild reads this on startup to locate the Lazarus source tree
254+ # and the FPC compiler. Without it, lazbuild emits:
255+ # Error: (lazbuild) Invalid Lazarus directory "": directory lcl not found
256+ #
257+ # The location lazbuild looks at is platform-specific:
258+ # Unix: $HOME/.lazarus/ (dotted)
259+ # Windows: %LOCALAPPDATA%\lazarus\ — NOT %APPDATA%, NOT dotted
260+ #
261+ # The Windows path comes from Lazarus's lazbaseconf.inc:
262+ # PrimaryConfigPath := ExtractFilePath(ChompPathDelim(
263+ # GetAppConfigDirUTF8(False))) + 'lazarus';
264+ # where GetAppConfigDir(False) on Windows resolves via
265+ # CSIDL_LOCAL_APPDATA to %LOCALAPPDATA%\<appname>\, so the parent +
266+ # 'lazarus' is %LOCALAPPDATA%\lazarus\ (no leading dot).
267+ #
268+ # On Windows, lazbuild is a native PE binary and expects Windows-
269+ # style paths in the XML values, so we cygpath them to backslash form.
270+ if [ " $IS_WINDOWS " = " 1" ]; then
271+ # On Windows runners $LOCALAPPDATA is set; fall back to the
272+ # well-known location if it isn't.
273+ WIN_LOCALAPPDATA=" ${LOCALAPPDATA:- $USERPROFILE / AppData/ Local} "
274+ LAZ_CFG_DIR=" $( cygpath -u " $WIN_LOCALAPPDATA " ) /lazarus"
275+ LAZ_DIR_NATIVE=" $( cygpath -w " $LAZARUS_DIR " ) "
276+ FPC_EXE_NATIVE=" $( cygpath -w " $FPC_EXE " ) "
277+ else
278+ LAZ_CFG_DIR=" ${HOME} /.lazarus"
279+ LAZ_DIR_NATIVE=" $LAZARUS_DIR "
280+ FPC_EXE_NATIVE=" $FPC_EXE "
281+ fi
282+
283+ mkdir -p " $LAZ_CFG_DIR "
284+
285+ cat > " $LAZ_CFG_DIR /environmentoptions.xml" << EOF
286+ <?xml version="1.0" encoding="UTF-8"?>
287+ <CONFIG>
288+ <EnvironmentOptions>
289+ <LazarusDirectory Value="$LAZ_DIR_NATIVE "/>
290+ <CompilerFilename Value="$FPC_EXE_NATIVE "/>
291+ </EnvironmentOptions>
292+ </CONFIG>
293+ EOF
294+
295+ # Put lazbuild on PATH for subsequent steps.
296+ export PATH=" $LAZARUS_DIR :$PATH "
297+ if [ -n " ${GITHUB_PATH:- } " ]; then
298+ if [ " $IS_WINDOWS " = " 1" ]; then
299+ cygpath -w " $LAZARUS_DIR " >> " $GITHUB_PATH "
300+ else
301+ echo " $LAZARUS_DIR " >> " $GITHUB_PATH "
302+ fi
303+ fi
304+
305+ lazbuild --version
306+ echo " FPC + Lazarus installation complete."
0 commit comments