Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 60 additions & 29 deletions build-aux/inno_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sys
import os
import re
import shutil
import glob
import itertools
Expand Down Expand Up @@ -41,6 +42,44 @@ def run_command(command, error_message):
sys.exit(1)


env_bin_dir = f"{build_environment_path}/bin"


def collect_dependencies(binary, out_dir):
"""Copy every DLL from the build environment that `binary` needs, transitively.

This reads the PE import table with objdump rather than using `ldd`. `ldd`
resolves dependencies by actually loading the binary, which deadlocks on
libraries that do work at load time - notably ANGLE's libGLESv2.dll, where
the installer build would hang indefinitely.

DLLs that do not exist in the build environment are Windows system DLLs and
are deliberately not packaged.
"""
seen = set()
queue = [binary]

while queue:
current = queue.pop()
result = subprocess.run(
["objdump", "-p", current], capture_output=True, text=True, errors="replace"
)
if result.returncode != 0:
print(f"Reading imports of {current} failed", file=sys.stderr)
sys.exit(1)

for name in re.findall(r"DLL Name:\s*(\S+)", result.stdout):
key = name.lower()
if key in seen:
continue
seen.add(key)

candidate = os.path.join(env_bin_dir, name)
if os.path.exists(candidate):
shutil.copy(candidate, out_dir)
queue.append(candidate)


# Collect DLLs
print("Collecting DLLs...", file=sys.stderr)
dlls_dir = os.path.join(build_root, "dlls")
Expand All @@ -50,40 +89,32 @@ def run_command(command, error_message):

os.mkdir(dlls_dir)

# Don't use os.path.join here, because that uses the wrong separators which breaks wildcard expansion.
run_command(
f"ldd {build_root}/{ui_output} | grep '\\/mingw.*\.dll' -o | xargs -i cp {{}} {dlls_dir}",
"Collecting app DLLs failed"
)
collect_dependencies(f"{build_root}/{ui_output}", dlls_dir)

for loader in glob.glob(f"{build_environment_path}/lib/gdk-pixbuf-2.0/2.10.0/loaders/*.dll"):
run_command(
f"ldd {loader} | grep '\\/mingw.*\.dll' -o | xargs -i cp {{}} {dlls_dir}",
f"Collecting pixbuf-loader ({loader}) DLLs failed"
)
collect_dependencies(loader, dlls_dir)

# ANGLE is loaded at runtime, so it is not in the import table of anything above
# and has to be packaged explicitly, together with what it depends on.
for angle_dll in itertools.chain(
glob.glob(f"{build_environment_path}/bin/libEGL*.dll"),
glob.glob(f"{build_environment_path}/bin/libGLES*.dll"),
glob.glob(f"{env_bin_dir}/libEGL*.dll"),
glob.glob(f"{env_bin_dir}/libGLES*.dll"),
):
run_command(
f"cp {angle_dll} {dlls_dir}",
f"Collecting angle ({angle_dll}) DLLs failed",
)
run_command(
f"ldd {angle_dll} | grep '\\/mingw.*\.dll' -o | xargs -i cp {{}} {dlls_dir}",
f"Collecting angle dependency ({angle_dll}) DLLs failed",
)

# add libcrypto-3-x64.dll and libssl-3-x64.dll
run_command(
f"cp {build_environment_path}/bin/libcrypto-3-x64.dll {dlls_dir}",
"Collecting libcrypto-3-x64.dll failed",
)
run_command(
f"cp {build_environment_path}/bin/libssl-3-x64.dll {dlls_dir}",
"Collecting libssl-3-x64.dll failed",
)
shutil.copy(angle_dll, dlls_dir)
collect_dependencies(angle_dll, dlls_dir)

# add the openssl runtime. The file name carries the architecture
# (libcrypto-3-x64.dll on x86_64, libcrypto-3-arm64.dll on aarch64), so glob it.
for openssl_pattern in ("libcrypto-3-*.dll", "libssl-3-*.dll"):
matches = glob.glob(f"{build_environment_path}/bin/{openssl_pattern}")
if not matches:
print(f"Could not find any openssl dll matching {openssl_pattern}", file=sys.stderr)
sys.exit(1)
for openssl_dll in matches:
run_command(
f"cp {openssl_dll} {dlls_dir}",
f"Collecting openssl ({openssl_dll}) failed",
)

# Collect necessary GSchema Xml's and compile them into a `gschemas.compiled`
print("Collecting and compiling GSchemas...", file=sys.stderr)
Expand Down
6 changes: 3 additions & 3 deletions build-aux/rnote_inno.iss.in
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ DisableProgramGroupPage=yes
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
; Run in 64-bit mode, restrict to x64compatible architecture.
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
; Run in 64-bit mode, restricted to the architecture this was built for.
ArchitecturesAllowed=@INNO_ARCH@
ArchitecturesInstallIn64BitMode=@INNO_ARCH@

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Expand Down
58 changes: 58 additions & 0 deletions crates/rnote-ui/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,64 @@ pub(crate) fn setup_env() -> anyhow::Result<()> {

//std::env::set_var("RUST_LOG", "rnote=debug,rnote-cli=debug,rnote-engine=debug,rnote-compose=debug");
}

// Windows on ARM has no native desktop OpenGL driver, so WGL is serviced
// by Microsoft's OpenGLOn12 mapping layer, which translates OpenGL onto
// Direct3D 12. That layer crashes with an access violation (0xc0000005)
// after a few minutes of normal use.
//
// Steering GDK away from WGL makes it pick EGL, and with it the ANGLE
// that is already shipped alongside the app, mapping GL ES onto
// Direct3D 11 instead. GPU rendering is kept: GSK still ends up on
// GskGLRenderer, it just no longer goes through OpenGLOn12.
//
// Only applied as a default, so GDK_DISABLE from the environment wins.
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
if std::env::var_os("GDK_DISABLE").is_none() {
// SAFETY: this setup only happens while still being single-threaded
unsafe {
std::env::set_var("GDK_DISABLE", "wgl");
}
}

// On Windows, pangocairo renders glyphs through cairo's win32 font
// backend, which goes to DirectWrite and Direct2D. On Windows on ARM
// that path is broken: pango logs "All font fallbacks failed" for every
// layout and rendering text eventually dies with an access violation
// (0xc0000005) in d2d1.dll. It reproducibly takes down the app when a
// document containing a text stroke is opened, or when the typewriter
// is used.
//
// The fontconfig/freetype backend works correctly here, so select it
// explicitly. Fontconfig is present and configured in the MSYS2
// environment the app is built and shipped with.
//
// Only applied as a default, so PANGOCAIRO_BACKEND from the environment
// wins, and scoped to aarch64 because the DirectWrite path was only
// verified to be broken there.
//
// This deliberately does not use `std::env::set_var`: on Windows that
// only calls SetEnvironmentVariableW, which updates the Win32
// environment block but not the copy the C runtime builds at startup.
// pangocairo reads the variable with plain `getenv` (unlike GDK, which
// uses `g_getenv` and therefore does see `set_var`), so it would never
// observe the value. `_putenv_s` updates the CRT table that `getenv`
// reads, and pangocairo resolves to the same UCRT as the app.
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
if std::env::var_os("PANGOCAIRO_BACKEND").is_none() {
unsafe extern "C" {
fn _putenv_s(
name: *const std::ffi::c_char,
value: *const std::ffi::c_char,
) -> std::ffi::c_int;
}

// SAFETY: this setup only happens while still being single-threaded,
// and both pointers are valid nul-terminated C string literals.
unsafe {
_putenv_s(c"PANGOCAIRO_BACKEND".as_ptr(), c"fc".as_ptr());
}
}
} else if cfg!(target_os = "macos") {
let canonicalized_exec_dir = exec_parent_dir()?.canonicalize()?;

Expand Down
95 changes: 88 additions & 7 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ log_level := "debug"
build_folder := "_mesonbuild"
flatpak_app_folder := "_flatpak_app"
flatpak_repo_folder := "_flatpak_repo"
mingw64_prefix_path := "C:/msys64/mingw64"
# Install prefix for Windows builds. Empty means "derive from the active MSYS2
# environment", which makes MINGW64 and CLANGARM64 work without editing this file.
# Override with e.g. `just msys_prefix_path=C:/msys64/mingw64 setup-win-installer`.
msys_prefix_path := ""

[private]
linux_distr := `grep -o -E '^ID=([a-zA-Z0-9_]*)$' -r /etc/os-release | cut -d= -f2 | tr '[:upper:]' '[:lower:]'`
Expand Down Expand Up @@ -83,12 +86,69 @@ prerequisites-dev: prerequisites
cargo binstall -y --locked cargo-nextest cargo-edit cargo-deny

# in MSYS2 shell
#
# Works in any MSYS2 environment (https://www.msys2.org/docs/environments/).
# The package prefix and the install prefix are taken from the environment, so
# MINGW64 (x86_64) and CLANGARM64 (aarch64, e.g. Snapdragon X) use the same recipes.
prerequisites-win:
#!/usr/bin/env bash
set -euxo pipefail

pacman -S --noconfirm \
unzip git mingw-w64-x86_64-xz mingw-w64-x86_64-pkgconf mingw-w64-x86_64-gcc mingw-w64-x86_64-clang \
mingw-w64-x86_64-toolchain mingw-w64-x86_64-autotools mingw-w64-x86_64-make mingw-w64-x86_64-cmake \
mingw-w64-x86_64-meson mingw-w64-x86_64-diffutils mingw-w64-x86_64-desktop-file-utils \
mingw-w64-x86_64-appstream mingw-w64-x86_64-gtk4 mingw-w64-x86_64-libadwaita mingw-w64-x86_64-angleproject
unzip git \
"${MINGW_PACKAGE_PREFIX}-xz" "${MINGW_PACKAGE_PREFIX}-pkgconf" \
"${MINGW_PACKAGE_PREFIX}-toolchain" "${MINGW_PACKAGE_PREFIX}-autotools" \
"${MINGW_PACKAGE_PREFIX}-make" "${MINGW_PACKAGE_PREFIX}-cmake" \
"${MINGW_PACKAGE_PREFIX}-meson" "${MINGW_PACKAGE_PREFIX}-diffutils" \
"${MINGW_PACKAGE_PREFIX}-desktop-file-utils" "${MINGW_PACKAGE_PREFIX}-appstream" \
"${MINGW_PACKAGE_PREFIX}-gtk4" "${MINGW_PACKAGE_PREFIX}-libadwaita" \
"${MINGW_PACKAGE_PREFIX}-angleproject"

if [[ "${MSYSTEM}" != "MINGW64" ]]; then
# Pin gtk before dcomp, for the same reason as the MINGW64 pinning below
# (only the package names and the repo path differ).
#
# Since the Win32 backend moved to DirectComposition, GSK cannot realize
# GL or Vulkan on a GdkWin32Toplevel unless a dcomp device exists, and
# silently degrades to GskCairoRenderer, i.e. pure software rendering.
# Verified on CLANGARM64 / Snapdragon X Elite: WGL 4.6 contexts are
# created fine on the Adreno X1-85, but every renderer still falls back
# to cairo with "OpenGL requires Direct Composition".
# gettext must be pinned along with it: gtk 4.18.6 and libadwaita 1.7.7
# import DllMain from libintl-8.dll, which newer gettext no longer
# exports. Without this the app dies at load time with
# STATUS_ENTRYPOINT_NOT_FOUND (0xC0000139) before printing anything.
repo_arch="$(echo "${MSYSTEM}" | tr '[:upper:]' '[:lower:]')"
pinned=(
"${MINGW_PACKAGE_PREFIX}-gettext-libtextstyle-0.26-1-any.pkg.tar.zst"
"${MINGW_PACKAGE_PREFIX}-gettext-runtime-0.26-1-any.pkg.tar.zst"
"${MINGW_PACKAGE_PREFIX}-gettext-tools-0.26-1-any.pkg.tar.zst"
"${MINGW_PACKAGE_PREFIX}-gtk4-4.18.6-3-any.pkg.tar.zst"
"${MINGW_PACKAGE_PREFIX}-libadwaita-1.7.7-1-any.pkg.tar.zst"
)
pin_dir="$(mktemp -d)"
for p in "${pinned[@]}"; do
curl -fsSL -o "${pin_dir}/${p}" "https://repo.msys2.org/mingw/${repo_arch}/${p}"
done
pacman -U --noconfirm "${pinned[@]/#/${pin_dir}/}"
rm -rf "${pin_dir}"

# Keep `pacman -Syu` from silently undoing the pin (and with it the GPU
# acceleration). Remove these from IgnorePkg to upgrade deliberately.
# Note: IgnorePkg only takes effect inside the [options] section,
# appending it to the end of the file lands in a repo section and is
# silently ignored (pacman only warns).
if ! grep -qE "^IgnorePkg.*${MINGW_PACKAGE_PREFIX}-gtk4" /etc/pacman.conf; then
sed -i "/^\[options\]/a IgnorePkg = ${MINGW_PACKAGE_PREFIX}-gtk4 ${MINGW_PACKAGE_PREFIX}-libadwaita ${MINGW_PACKAGE_PREFIX}-gettext-runtime ${MINGW_PACKAGE_PREFIX}-gettext-libtextstyle ${MINGW_PACKAGE_PREFIX}-gettext-tools" \
/etc/pacman.conf
fi

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
exit 0
fi

# Everything below is a workaround for x86_64/GCC-specific breakage in the
# MINGW64 environment and is deliberately skipped elsewhere.
mv /mingw64/lib/libpthread.dll.a /mingw64/lib/libpthread.dll.a.bak
# We need to pin version : cairo before dwrite, gtk before dcomp
# cairo : before
Expand Down Expand Up @@ -145,13 +205,34 @@ setup-release *MESON_ARGS:
{{ MESON_ARGS }} \
{{ build_folder }}

# in MINGW64 shell
# Dev build in a MSYS2 shell (MINGW64, CLANGARM64, ..). Unlike `setup-dev` this
# installs into the MSYS2 prefix instead of /usr, so the app can be run locally
# without building the installer.
setup-win-dev *MESON_ARGS:
#!/usr/bin/env bash
set -euxo pipefail
prefix="{{ msys_prefix_path }}"
[[ -n "$prefix" ]] || prefix="$(cygpath -m "$MSYSTEM_PREFIX")"
meson setup \
--prefix="$prefix" \
-Dprofile=devel \
-Dwin-build-environment-path="$prefix" \
-Dci={{ ci }} \
{{ MESON_ARGS }} \
{{ build_folder }}

# in a MSYS2 shell (MINGW64, CLANGARM64, ..)
setup-win-installer installer_name="rnote-win-installer":
#!/usr/bin/env bash
set -euxo pipefail
prefix="{{ msys_prefix_path }}"
[[ -n "$prefix" ]] || prefix="$(cygpath -m "$MSYSTEM_PREFIX")"
meson setup \
--prefix={{ mingw64_prefix_path }} \
--prefix="$prefix" \
-Dprofile=default \
-Dcli=true \
-Dwin-installer-name={{ installer_name }} \
-Dwin-build-environment-path="$prefix" \
-Dci={{ ci }} \
{{ build_folder }}

Expand Down
9 changes: 9 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,15 @@ if build_ui == true
inno_script_conf.set_quoted('UI_OUTPUT', ui_output)
inno_script_conf.set_quoted('INSTALLER_NAME', win_installer_name)

# Inno-Setup architecture identifier, derived from the build so an aarch64
# build produces an installer that actually runs on Windows on ARM.
if host_machine.cpu_family() == 'aarch64'
inno_arch = 'arm64'
else
inno_arch = 'x64compatible'
endif
inno_script_conf.set('INNO_ARCH', inno_arch)

inno_script = configure_file(
input: 'build-aux/rnote_inno.iss.in',
output: 'rnote_inno.iss',
Expand Down
Loading