bump(main/ghc): 9.12.2

Patched `rts` heap reservation logic on Android:

  * Need for patch

  - There seems to be a bug on Android. Sometimes simple commands like `ghc --help` usage 90% - 100% cpu and hangs.

  - It doesn't happen every time, but ~25% of the times (at least in my
    testing).

  * Cause of the bug

  - The function `osReserveHeapMemory` tries to allocate virtual space starting
    from `0x4200000000`. The allocated space has to start at an address >= this address.

  - If the kernel doesn't allocate inside this space, it keeps on repeating the
    `mmap` call with increasing starting point.

  - Now, on Android the kernel sometimes return an address above (as in counting)
    this `hint` address. It repeatedly returns the same address for subsequent calls.

  - Thus, an infinite loop occurs.

  References:
    - 383be28ffd/rts/posix/OSMem.c (L461)
    - https://github.com/termux/termux-packages/pull/22991#issuecomment-2759137291

  * Solution (proposed by Robert Kirkman):

  - It introduces a new helper function `osTryReserveHeapMemoryRecursive`.
    This transforms the heap reservation logic into a recursive one.

  - `osTryReserveHeapMemory()` is run multiple times without unmapping the
    undesired addresses. Thus, forcing the kernel to map subsequent calls of
    `mmap` to a new, unique address until an address above the `0x4200000000`
    mark is obtained.

  - After which each recursive call unmaps its undesired address before returning
    the desired address (in order from last mapped to first mapped).

  References:
    - https://gitlab.haskell.org/ghc/ghc/-/merge_requests/14164
    - https://github.com/termux/termux-packages/pull/22991#issuecomment-2761325484

Co-authored-by: Robert Kirkman <rkirkman@termux.dev>
Signed-off-by: Aditya Alok <alok@termux.dev>
This commit is contained in:
termux-pacman-bot
2025-04-08 16:42:00 +00:00
parent 5f10087ffd
commit 7a4ab56ecb
14 changed files with 444 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
--- ghc-9.12.1/compiler/GHC/Driver/DynFlags.hs 2024-12-16 00:59:45.000000000 +0530
+++ ghc-9.12.1.mod/compiler/GHC/Driver/DynFlags.hs 2025-01-04 19:52:30.600705081 +0530
@@ -1311,6 +1311,8 @@
(OSDarwin, ArchAArch64) -> [Opt_PIC]
(OSLinux, ArchAArch64) -> [Opt_PIC, Opt_ExternalDynamicRefs]
(OSLinux, ArchARM {}) -> [Opt_PIC, Opt_ExternalDynamicRefs]
+ (OSLinux, ArchX86_64) -> [Opt_PIC, Opt_ExternalDynamicRefs]
+ (OSLinux, ArchX86) -> [Opt_PIC, Opt_ExternalDynamicRefs]
(OSLinux, ArchRISCV64 {}) -> [Opt_PIC, Opt_ExternalDynamicRefs]
(OSOpenBSD, ArchX86_64) -> [Opt_PIC] -- Due to PIE support in
-- OpenBSD since 5.3 release

112
packages/ghc/build.sh Normal file
View File

@@ -0,0 +1,112 @@
TERMUX_PKG_HOMEPAGE=https://www.haskell.org/ghc/
TERMUX_PKG_DESCRIPTION="The Glasgow Haskell Compiler"
TERMUX_PKG_LICENSE="custom"
TERMUX_PKG_MAINTAINER="Aditya Alok <alok@termux.dev>"
TERMUX_PKG_VERSION=9.12.2
TERMUX_PKG_SRCURL="https://downloads.haskell.org/~ghc/$TERMUX_PKG_VERSION/ghc-$TERMUX_PKG_VERSION-src.tar.xz"
TERMUX_PKG_SHA256=0e49cd5dde43f348c5716e5de9a5d7a0f8d68d945dc41cf75dfdefe65084f933
TERMUX_PKG_DEPENDS="libiconv, libffi, libgmp, libandroid-posix-semaphore, ncurses"
TERMUX_PKG_BUILD_IN_SRC=true
TERMUX_PKG_EXTRA_CONFIGURE_ARGS="
--host=$TERMUX_BUILD_TUPLE
--with-system-libffi
--disable-ld-override"
TERMUX_PKG_NO_STATICSPLIT=true
TERMUX_PKG_LICENSE_FILE="LICENSE"
TERMUX_PKG_REPLACES="ghc-libs-static, ghc-libs"
TERMUX_PKG_PROVIDES="ghc-libs, ghc-libs-static"
__setup_bootstrap_compiler() {
local version=9.10.1
local temp_folder="$TERMUX_PKG_CACHEDIR/ghc-bootstrap-$version"
local tarball="$temp_folder.tar.xz"
local runtime_folder="$temp_folder-runtime"
export PATH="$runtime_folder/bin:$PATH"
[[ -d "$runtime_folder" ]] && return
termux_download "https://downloads.haskell.org/~ghc/$version/ghc-$version-x86_64-ubuntu20_04-linux.tar.xz" \
"$tarball" \
ae3be406fdb73bd2b0c22baada77a8ff2f8cde6220dd591dc24541cfe9d895eb
mkdir -p "$temp_folder" "$runtime_folder"
tar xf "$tarball" --strip-components=1 -C "$temp_folder"
(
set -e
unset CC CXX CFLAGS CXXFLAGS CPPFLAGS LDFLAGS AR AS CPP LD RANLIB READELF STRIP
cd "$temp_folder"
./configure --prefix="$runtime_folder"
make install
) &>/dev/null
rm -Rf "$temp_folder" "$tarball"
}
termux_step_pre_configure() {
__setup_bootstrap_compiler && termux_setup_cabal
export CONF_CC_OPTS_STAGE2="$CFLAGS $CPPFLAGS"
export CONF_GCC_LINKER_OPTS_STAGE2="$LDFLAGS"
export CONF_CXX_OPTS_STAGE2="$CXXFLAGS"
export target="$TERMUX_HOST_PLATFORM"
export no_profiled_libs=""
if [[ "$TERMUX_ARCH" == "arm" ]]; then
target="armv7a-linux-androideabi"
# NOTE: We do not build profiled libs for arm. It exceeds the 6 hours usage
# limit of github CI.
no_profiled_libs="+no_profiled_libs"
fi
TERMUX_PKG_EXTRA_CONFIGURE_ARGS="$TERMUX_PKG_EXTRA_CONFIGURE_ARGS --target=$target"
./boot.source
}
termux_step_make() {
(
unset CFLAGS LDFLAGS CPPFLAGS # For hadrian compilation
./hadrian/build binary-dist-dir \
-j"$TERMUX_PKG_MAKE_PROCESSES" \
--flavour="release$no_profiled_libs" --docs=none \
"stage1.unix.ghc.link.opts += -optl-landroid-posix-semaphore"
)
}
termux_step_make_install() {
cd _build/bindist/ghc-"$TERMUX_PKG_VERSION"-"$target" || exit 1
# Remove unnecessary flags. They would get written to settings file otherwise:
unset CONF_CC_OPTS_STAGE2 CONF_GCC_LINKER_OPTS_STAGE2 CONF_CXX_OPTS_STAGE2
# We need to re-run configure:
# See: https://gitlab.haskell.org/ghc/ghc/-/issues/22058
./configure \
--prefix="$TERMUX_PREFIX" \
--host="$target"
HOST_GHC_PKG="$(realpath ../../stage0/bin/ghc-pkg)" make install
}
termux_step_post_massage() {
local ghclibs_dir="lib/ghc-$TERMUX_PKG_VERSION"
if ! [[ -d "$ghclibs_dir" ]]; then
echo "ERROR: GHC lib directory is not at expected place. Please verify before continuing."
exit 1
fi
# Remove version suffix from `llc` and `opt`:
sed -i -E 's|("LLVM llc command",) "llc.*"|\1 "llc"|' "$ghclibs_dir"/lib/settings
sed -i -E 's|("LLVM opt command",) "opt.*"|\1 "opt"|' "$ghclibs_dir"/lib/settings
# Remove cross-prefix from tools:
sed -i "s|$CC|${CC/${target}-/}|g" "$ghclibs_dir"/lib/settings
sed -i "s|$CXX|${CXX/${target}-/}|g" "$ghclibs_dir"/lib/settings
# Strip unneeded symbols:
find . -type f \( -name "*.so" -o -name "*.a" \) -exec "$STRIP" --strip-unneeded {} \;
find "$ghclibs_dir"/bin -type f -exec "$STRIP" {} \;
}

View File

@@ -0,0 +1,11 @@
--- ghc-9.12.1/libraries/Cabal/Cabal/src/Distribution/Simple/InstallDirs.hs 2024-12-16 01:00:10.000000000 +0530
+++ ghc-9.12.1.mod/libraries/Cabal/Cabal/src/Distribution/Simple/InstallDirs.hs 2025-01-04 20:05:06.846734461 +0530
@@ -205,7 +205,7 @@
windowsProgramFilesDir <- getWindowsProgramFilesDir
return (windowsProgramFilesDir </> "Haskell")
Haiku -> return "/boot/system/non-packaged"
- _ -> return "/usr/local"
+ _ -> return "@TERMUX_PREFIX@"
installLibDir <-
case buildOS of
Windows -> return "$prefix"

View File

@@ -0,0 +1,18 @@
--- ghc-9.12.1/m4/fp_gcc_supports_no_pie.m4 2025-02-24 22:46:43.263794157 +0530
+++ ghc-9.12.1.mod/m4/fp_gcc_supports_no_pie.m4 2025-03-25 14:39:14.331731419 +0530
@@ -6,15 +6,6 @@
[
AC_REQUIRE([AC_PROG_CC])
AC_MSG_CHECKING([whether CC supports -no-pie])
- echo 'int main() { return 0; }' > conftest.c
- "$CC" $CONF_GCC_CC_OPTS_STAGE2 -c conftest.c
- # Some GCC versions only warn when passed an unrecognized flag.
- if "$CC" $CONF_GCC_LINKER_OPTS_STAGE2 -no-pie -Werror conftest.o -o conftest > conftest.txt 2>&1 && ! grep -i unrecognized conftest.txt > /dev/null 2>&1; then
- CONF_GCC_SUPPORTS_NO_PIE=YES
- AC_MSG_RESULT([yes])
- else
CONF_GCC_SUPPORTS_NO_PIE=NO
AC_MSG_RESULT([no])
- fi
- rm -f conftest.c conftest.o conftest
])

View File

@@ -0,0 +1,14 @@
--- ghc-9.12.1/rts/linker/ElfTypes.h 2024-12-16 00:59:46.000000000 +0530
+++ ghc-9.12.1.mod/rts/linker/ElfTypes.h 2025-01-04 21:31:55.550538431 +0530
@@ -8,6 +8,11 @@
#include <elf.h>
#include "linker/InitFini.h"
+#define ELF32_ST_VISIBILITY(o) ((o) & 0x03)
+
+/* For ELF64 the definitions are the same. */
+#define ELF64_ST_VISIBILITY(o) ELF32_ST_VISIBILITY(o)
+
/*
* Define a set of types which can be used for both ELF32 and ELF64
*/

View File

@@ -0,0 +1,11 @@
--- ghc-9.12.1/m4/fptools_set_haskell_platform_vars.m4 2024-12-16 00:59:46.000000000 +0530
+++ ghc-9.12.1.mod/m4/fptools_set_haskell_platform_vars.m4 2025-01-24 18:18:00.513871875 +0530
@@ -21,7 +21,7 @@
s390x)
test -z "[$]2" || eval "[$]2=ArchS390X"
;;
- arm)
+ arm*)
GET_ARM_ISA()
test -z "[$]2" || eval "[$]2=\"ArchARM \$ARM_ISA \$ARM_ISA_EXT \$ARM_ABI\""
;;

View File

@@ -0,0 +1,13 @@
--- ghc-9.12.1/hadrian/bindist/config.mk.in 2025-02-24 22:46:43.665077469 +0530
+++ ghc-9.12.1.mod/hadrian/bindist/config.mk.in 2025-02-26 02:09:44.451291933 +0530
@@ -130,8 +130,8 @@
#-----------------------------------------------------------------------------
# Build configuration
-CrossCompiling = @CrossCompiling@
-CrossCompilePrefix = @CrossCompilePrefix@
+CrossCompiling = NO
+CrossCompilePrefix =
GhcUnregisterised = @Unregisterised@
EnableDistroToolchain = @SettingsUseDistroMINGW@

View File

@@ -0,0 +1,22 @@
--- ghc-9.12.1/hadrian/src/Rules/BinaryDist.hs 2025-02-24 22:46:43.666788988 +0530
+++ ghc-9.12.1.mod/hadrian/src/Rules/BinaryDist.hs 2025-02-25 16:56:59.718827035 +0530
@@ -231,6 +231,6 @@
--
-- N.B. the ghc-pkg executable may be prefixed with a target triple
-- (c.f. #20267).
- ghcPkgName <- programName (vanillaContext Stage1 ghcPkg)
- cmd_ (bindistFilesDir -/- "bin" -/- ghcPkgName) ["recache"]
+ stage0ghcPkgPath <- programPath =<< programContext (Stage0 InTreeLibs) ghcPkg
+ cmd_ stage0ghcPkgPath ["recache"]
--- ghc-9.12.1/hadrian/bindist/Makefile 2025-02-24 22:46:43.665077469 +0530
+++ ghc-9.12.1.mod/hadrian/bindist/Makefile 2025-02-24 22:46:44.140454443 +0530
@@ -253,7 +253,7 @@
@echo "Updating the package DB"
$(foreach p, $(PKG_CONFS),\
$(call patchpackageconf,$(shell echo $(notdir $p) | sed 's/-[0-9.]*-[0-9a-zA-Z]*\.conf//g'),$(shell echo "$p" | sed 's:\0xxx\0: :g'),$(docdir),$(shell mk/relpath.sh "$(ActualLibsDir)" "$(docdir)"),$(shell echo $(notdir $p) | sed 's/.conf//g')))
- '$(DESTDIR)$(ActualBinsDir)/$(CrossCompilePrefix)ghc-pkg' --global-package-db "$(DESTDIR)$(ActualLibsDir)/package.conf.d" recache
+ '$(HOST_GHC_PKG)' --global-package-db "$(DESTDIR)$(ActualLibsDir)/package.conf.d" recache
.PHONY: install_mingw
install_mingw:

View File

@@ -0,0 +1,13 @@
# This is the culprit that removes `-fPIC` flag for i686 build.
--- ghc-9.12.1/hadrian/src/Settings/Packages.hs 2025-02-24 22:46:43.668323779 +0530
+++ ghc-9.12.1.mod/hadrian/src/Settings/Packages.hs 2025-02-25 23:06:51.153991428 +0530
@@ -510,7 +510,7 @@
speedHack :: Action Bool
speedHack = do
i386 <- anyTargetArch [ArchX86]
- goodOS <- not <$> anyTargetOs [OSSolaris2]
+ goodOS <- not <$> anyTargetOs [OSSolaris2, OSLinux]
return $ i386 && goodOS
-- See @rts/ghc.mk@.

View File

@@ -0,0 +1,11 @@
--- ghc-9.12.1/hadrian/src/Packages.hs 2025-02-24 22:46:43.665442228 +0530
+++ ghc-9.12.1.mod/hadrian/src/Packages.hs 2025-02-26 14:41:51.865132206 +0530
@@ -171,7 +171,7 @@
crossPrefix = do
cross <- flag CrossCompiling
targetPlatform <- setting TargetPlatformFull
- return $ if cross then targetPlatform ++ "-" else ""
+ return ""
-- | Given a 'Context', compute the name of the program that is built in it
-- assuming that the corresponding package's type is 'Program'. For example, GHC

View File

@@ -0,0 +1,18 @@
--- ghc-9.12.2/hadrian/src/Settings/Default.hs 2025-03-29 15:32:47.000000000 +0530
+++ ghc-9.12.2.mod/hadrian/src/Settings/Default.hs 2025-04-07 15:04:48.724844187 +0530
@@ -172,13 +172,11 @@
, unlit
, xhtml
, if winTarget then win32 else unix
- ]
- , when (not cross)
- [ haddock
+ , haddock
, hpcBin
, iserv
, runGhc
- , ghcToolchainBin
+ , terminfo
]
, when (winTarget && not cross)
[ -- See Note [Hadrian's ghci-wrapper package]

View File

@@ -0,0 +1,46 @@
--- ghc-9.12.2/hadrian/src/Settings/Packages.hs 2025-03-29 15:32:47.000000000 +0530
+++ ghc-9.12.2.mod/hadrian/src/Settings/Packages.hs 2025-04-07 15:06:41.398290889 +0530
@@ -85,8 +85,8 @@
-- (#14335) and completely untested in CI for cross
-- backends at the moment, so we might as well disable it
-- for cross GHC.
- [ andM [expr ghcWithInterpreter, notStage0, notCross] `cabalFlag` "internal-interpreter"
- , notM cross `cabalFlag` "terminfo"
+ [ andM [expr ghcWithInterpreter, notStage0] `cabalFlag` "internal-interpreter"
+ , notStage0 `cabalFlag` "terminfo"
, arg "-build-tool-depends"
, flag UseLibzstd `cabalFlag` "with-libzstd"
-- ROMES: While the boot compiler is not updated wrt -this-unit-id
@@ -121,7 +121,7 @@
-------------------------------- ghcPkg --------------------------------
, package ghcPkg ?
- builder (Cabal Flags) ? notM cross `cabalFlag` "terminfo"
+ builder (Cabal Flags) ? notStage0 `cabalFlag` "terminfo"
-------------------------------- ghcPrim -------------------------------
, package ghcPrim ? mconcat
@@ -213,7 +213,7 @@
-- dependencies.
-- TODO: Perhaps the user should rather be responsible for this?
, package haskeline ?
- builder (Cabal Flags) ? notM cross `cabalFlag` "terminfo"
+ builder (Cabal Flags) ? notStage0 `cabalFlag` "terminfo"
-------------------------------- terminfo ------------------------------
, package terminfo ?
@@ -374,10 +374,10 @@
, input "**/RtsUtils.c" ? pure
[ "-DProjectVersion=" ++ show projectVersion
- , "-DHostPlatform=" ++ show hostPlatform
- , "-DHostArch=" ++ show hostArch
- , "-DHostOS=" ++ show hostOs
- , "-DHostVendor=" ++ show hostVendor
+ , "-DHostPlatform=" ++ show targetPlatform
+ , "-DHostArch=" ++ show targetArch
+ , "-DHostOS=" ++ show targetOs
+ , "-DHostVendor=" ++ show targetVendor
, "-DBuildPlatform=" ++ show buildPlatform
, "-DBuildArch=" ++ show buildArch
, "-DBuildOS=" ++ show buildOs

View File

@@ -0,0 +1,12 @@
--- ghc-9.12.2/hadrian/src/Rules/Program.hs 2025-03-29 15:32:47.000000000 +0530
+++ ghc-9.12.2.mod/hadrian/src/Rules/Program.hs 2025-04-07 23:55:57.642949426 +0530
@@ -102,9 +102,6 @@
cross <- flag CrossCompiling
-- For cross compiler, copy @stage0/bin/<pgm>@ to @stage1/bin/@.
case (cross, stage) of
- (True, s) | s > stage0InTree -> do
- srcDir <- buildRoot <&> (-/- (stageString stage0InTree -/- "bin"))
- copyFile (srcDir -/- takeFileName bin) bin
_ -> buildBinary rs bin ctx
buildBinary :: [(Resource, Int)] -> FilePath -> Context -> Action ()

View File

@@ -0,0 +1,132 @@
From fdd0c67a38ef8435b9aa46c0d3c9d4460191cdcf Mon Sep 17 00:00:00 2001
From: Robert Kirkman <rkirkman@termux.dev>
Date: Fri, 28 Mar 2025 16:53:16 -0500
Subject: [PATCH] rts: reattempt heap reservation recursively before unmapping
addresses below the 8GB mark
This patch works around Android's mmap() occasionally repeatedly mapping the exact
same block of memory at an address below the 8GB mark that was just
unmapped, which would cause the 'ghc --help' command to fall into an infinite loop while
osTryReserveHeapMemory() repeatedly returned the same unwanted address. This
moves the heap reservation attempt logic into a recursive function that
runs osTryReserveHeapMemory() multiple times without unmapping the
undesired addresses, to force the mapping of new, unique addresses
until an address above the 8GB mark is obtained,
after which each recursive call unmaps its undesired address before
returning the desired address, in order from last mapped to first mapped.
First discussed here: https://github.com/termux/termux-packages/pull/22991
---
rts/posix/OSMem.c | 88 +++++++++++++++++++++++++----------------------
1 file changed, 46 insertions(+), 42 deletions(-)
diff --git a/rts/posix/OSMem.c b/rts/posix/OSMem.c
index 94c60f441ac9..2f1638bb5123 100644
--- a/rts/posix/OSMem.c
+++ b/rts/posix/OSMem.c
@@ -493,11 +493,53 @@ osTryReserveHeapMemory (W_ len, void *hint)
return start;
}
-void *osReserveHeapMemory(void *startAddressPtr, W_ *len)
+static void *
+osTryReserveHeapMemoryRecursive(W_ minimumAddress, W_ startAddress, int attempt, W_ *len)
{
- int attempt;
- void *at;
+ *len &= ~MBLOCK_MASK;
+
+ if (*len < MBLOCK_SIZE) {
+ // Give up if the system won't even give us 16 blocks worth of heap
+ barf("osReserveHeapMemory: Failed to allocate heap storage");
+ }
+
+ void *hint = (void*)(startAddress + attempt * BLOCK_SIZE);
+ void *at = osTryReserveHeapMemory(*len, hint);
+ attempt++;
+
+ if (at == NULL) {
+ // This means that mmap failed which we take to mean that we asked
+ // for too much memory. This can happen due to POSIX resource
+ // limits. In this case we reduce our allocation request by a
+ // fraction of the current size and try again.
+ //
+ // Note that the previously would instead decrease the request size
+ // by a factor of two; however, this meant that significant amounts
+ // of memory will be wasted (e.g. imagine a machine with 512GB of
+ // physical memory but a 511GB ulimit). See #14492.
+ *len -= *len / 8;
+ // debugBelch("Limit hit, reduced len: %zu\n", *len);
+ return osTryReserveHeapMemoryRecursive(minimumAddress, startAddress, attempt, len);
+ } else if ((W_)at >= minimumAddress) {
+ // Success! We were given a block of memory starting above the 8 GB
+ // mark, which is what we were looking for.
+
+ return at;
+ } else {
+ // We got addressing space but it wasn't above the 8GB mark.
+ // Try again recursively first, unmap after, because on aarch64 Android,
+ // sometimes mmap() will continuously map the same address regardless of
+ // the hint changing, if that address has already been unmapped.
+ void *next_at = osTryReserveHeapMemoryRecursive(minimumAddress, startAddress, attempt, len);
+ if (munmap(at, *len) < 0) {
+ sysErrorBelch("unable to release reserved heap");
+ }
+ return next_at;
+ }
+}
+void *osReserveHeapMemory(void *startAddressPtr, W_ *len)
+{
/* We want to ensure the heap starts at least 8 GB inside the address space,
since we want to reserve the address space below that address for code.
Specifically, we need to make sure that any dynamically loaded code will
@@ -585,45 +627,7 @@ void *osReserveHeapMemory(void *startAddressPtr, W_ *len)
}
#endif
- attempt = 0;
- while (1) {
- *len &= ~MBLOCK_MASK;
-
- if (*len < MBLOCK_SIZE) {
- // Give up if the system won't even give us 16 blocks worth of heap
- barf("osReserveHeapMemory: Failed to allocate heap storage");
- }
-
- void *hint = (void*)(startAddress + attempt * BLOCK_SIZE);
- at = osTryReserveHeapMemory(*len, hint);
- if (at == NULL) {
- // This means that mmap failed which we take to mean that we asked
- // for too much memory. This can happen due to POSIX resource
- // limits. In this case we reduce our allocation request by a
- // fraction of the current size and try again.
- //
- // Note that the previously would instead decrease the request size
- // by a factor of two; however, this meant that significant amounts
- // of memory will be wasted (e.g. imagine a machine with 512GB of
- // physical memory but a 511GB ulimit). See #14492.
- *len -= *len / 8;
- // debugBelch("Limit hit, reduced len: %zu\n", *len);
- } else if ((W_)at >= minimumAddress) {
- // Success! We were given a block of memory starting above the 8 GB
- // mark, which is what we were looking for.
-
- break;
- } else {
- // We got addressing space but it wasn't above the 8GB mark.
- // Try again.
- if (munmap(at, *len) < 0) {
- sysErrorBelch("unable to release reserved heap");
- }
- }
- attempt++;
- }
-
- return at;
+ return osTryReserveHeapMemoryRecursive(minimumAddress, startAddress, 0, len);
}
void osCommitMemory(void *at, W_ size)
--
GitLab