diff --git a/.conan/config/global.conf b/.conan/config/global.conf new file mode 100644 index 00000000000..ab423990b31 --- /dev/null +++ b/.conan/config/global.conf @@ -0,0 +1,5 @@ +core.net.http:timeout = 300 + +tools.system.package_manager:mode = install +tools.system.package_manager:sudo = True + diff --git a/.conan/profiles/macos-latest-armv8.conanprofile b/.conan/profiles/macos-latest-armv8.conanprofile new file mode 100644 index 00000000000..67da6519295 --- /dev/null +++ b/.conan/profiles/macos-latest-armv8.conanprofile @@ -0,0 +1,11 @@ +[settings] +arch=armv8 +build_type=RelWithDebInfo +compiler=apple-clang +compiler.cppstd=17 +compiler.libcxx=libc++ +compiler.version=17 +os=Macos +# Keep in sync with CMAKE_OSX_DEPLOYMENT_TARGET in CMakeLists.txt +os.version=13.0 + diff --git a/.conan/profiles/ubuntu-latest-x86_64.conanprofile b/.conan/profiles/ubuntu-latest-x86_64.conanprofile new file mode 100644 index 00000000000..e516cd1d53c --- /dev/null +++ b/.conan/profiles/ubuntu-latest-x86_64.conanprofile @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=RelWithDebInfo +compiler=gcc +compiler.cppstd=gnu17 +compiler.libcxx=libstdc++11 +compiler.version=11 +os=Linux diff --git a/.conan/profiles/windows-latest-x86_64.conanprofile b/.conan/profiles/windows-latest-x86_64.conanprofile new file mode 100644 index 00000000000..1d62d9240cf --- /dev/null +++ b/.conan/profiles/windows-latest-x86_64.conanprofile @@ -0,0 +1,9 @@ +[settings] +arch=x86_64 +build_type=Release +compiler=msvc +compiler.cppstd=17 +compiler.runtime=dynamic +compiler.runtime_type=Debug +compiler.version=194 +os=Windows diff --git a/.github/workflows/conan.yml b/.github/workflows/conan.yml new file mode 100644 index 00000000000..bff9fdf492c --- /dev/null +++ b/.github/workflows/conan.yml @@ -0,0 +1,60 @@ +name: Build with conan + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + name: Building ${{ matrix.build_type }} ${{ matrix.profile }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + # Release build type + - os: ubuntu-latest + profile: ubuntu-latest-x86_64 + build_type: Release + - os: macos-latest + profile: macos-latest-armv8 + build_type: Release + #- os: windows-latest + # profile: windows-latest-x86_64 + # build_type: Release + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install Conan + uses: conan-io/setup-conan@v1 + + - name: Add artifactory & set conan config + run: | + conan config install ./.conan/config/global.conf + conan remote remove conancenter + conan remote add oc https://artifactory.owncloud-demo.com/artifactory/api/conan/conan-local + conan remote login oc ${{ secrets.CONAN_USER }} -p ${{ secrets.CONAN_PASSWORD }} + conan remote list + conan profile show + + - name: Create build directory + run: mkdir build + + - name: Install dependencies with conan + run: | + conan install . --profile=.conan/profiles/${{ matrix.profile }}.conanprofile -s build_type=${{ matrix.build_type }} + + - name: Build the client + working-directory: ./build + run: | + cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + cmake --build . + + - name: Run tests + working-directory: ./build + run: ctest --output-on-failure diff --git a/.gitignore b/.gitignore index 3b8359b14af..3c8f116185e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +CMakeUserPresets.json /branding/ .gitmodules *build*/ @@ -180,3 +181,11 @@ test/gui/config.ini test/gui/reports */**/__pycache__/ test/gui/users.json + +# conan generated files +#*-config.cmake +#*-data.cmake +#*-version.cmake +#*-release.cmake +#*Targets.cmake +#*conan*.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index c7f937934e5..9bda1b0fc3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Set this before the first project definition +# Keep in sync with os.version in .conan/profiles/macos-*.conanprofile set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0" CACHE STRING "Minimum macOS deployment version" FORCE) include(VERSION.cmake) @@ -23,13 +24,16 @@ set(APPLE_SUPPRESS_X11_WARNING ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -find_package(QT 6.8 NAMES Qt6 COMPONENTS Core REQUIRED) +find_package(QT 6.11 NAMES Qt6 COMPONENTS Core REQUIRED) -find_package(Qt6 COMPONENTS Core Concurrent Network Widgets Xml Quick QuickWidgets QuickControls2 REQUIRED) -find_package(Qt6LinguistTools REQUIRED) -get_target_property (QT_QMAKE_EXECUTABLE Qt::qmake IMPORTED_LOCATION) +find_package(Qt6 COMPONENTS Core Concurrent Network Widgets Xml Quick Qml QuickWidgets QuickControls2 REQUIRED) +find_package(Qt6 CONFIG REQUIRED COMPONENTS LinguistTools) + +get_target_property (QT_QMAKE_EXECUTABLE Qt6::qmake IMPORTED_LOCATION) message(STATUS "Using Qt ${QT_VERSION} (${QT_QMAKE_EXECUTABLE})") +qt6_standard_project_setup() + if (UNIX AND NOT APPLE) find_package(Qt6 REQUIRED COMPONENTS DBus) endif() diff --git a/admin/linux/create-appimage.sh b/admin/linux/create-appimage.sh new file mode 100755 index 00000000000..ce3e7656c6c --- /dev/null +++ b/admin/linux/create-appimage.sh @@ -0,0 +1,274 @@ +#!/bin/bash +set -euo pipefail + +usage() { + echo "Usage: $0 -b BUILD_DIR [-o OUTPUT_DIR] [-v VERSION]" + echo + echo "Create an AppImage from an already-built ownCloud client." + echo + echo "Options:" + echo " -b BUILD_DIR Path to the CMake build directory (required)" + echo " -o OUTPUT_DIR Where to place the resulting AppImage (default: .)" + echo " -v VERSION Version string for the AppImage filename (auto-detected from VERSION.cmake if omitted)" + exit 1 +} + +BUILD_DIR="" +OUTPUT_DIR="$(pwd)" +VERSION="" + +while getopts "b:o:v:h" opt; do + case "$opt" in + b) BUILD_DIR="$OPTARG" ;; + o) OUTPUT_DIR="$OPTARG" ;; + v) VERSION="$OPTARG" ;; + h) usage ;; + *) usage ;; + esac +done + +if [ -z "$BUILD_DIR" ]; then + echo "Error: -b BUILD_DIR is required" + usage +fi + +BUILD_DIR="$(cd "$BUILD_DIR" && pwd)" +OUTPUT_DIR="$(mkdir -p "$OUTPUT_DIR" && cd "$OUTPUT_DIR" && pwd)" +SOURCE_DIR="$(cd "$(dirname "$0")/../.." && pwd)" + +# Auto-detect version from VERSION.cmake if not provided +if [ -z "$VERSION" ]; then + version_file="$SOURCE_DIR/VERSION.cmake" + if [ ! -f "$version_file" ]; then + echo "Error: VERSION.cmake not found at $version_file and no -v given" + exit 1 + fi + MAJOR=$(grep 'MIRALL_VERSION_MAJOR' "$version_file" | sed 's/.*MIRALL_VERSION_MAJOR[[:space:]]*\([0-9]*\).*/\1/') + MINOR=$(grep 'MIRALL_VERSION_MINOR' "$version_file" | sed 's/.*MIRALL_VERSION_MINOR[[:space:]]*\([0-9]*\).*/\1/') + PATCH=$(grep 'MIRALL_VERSION_PATCH' "$version_file" | sed 's/.*MIRALL_VERSION_PATCH[[:space:]]*\([0-9]*\).*/\1/') + VERSION="${MAJOR}.${MINOR}.${PATCH}" + echo "Auto-detected version: $VERSION" +fi + +# Read APPLICATION_EXECUTABLE from the branding/OEM.cmake or OWNCLOUD.cmake +APP_EXECUTABLE="" +for cmake_file in "$SOURCE_DIR/branding/OEM.cmake" "$SOURCE_DIR/OWNCLOUD.cmake"; do + if [ -f "$cmake_file" ]; then + APP_EXECUTABLE=$(grep 'APPLICATION_EXECUTABLE' "$cmake_file" | head -1 | sed 's/.*APPLICATION_EXECUTABLE[[:space:]]*"\([^"]*\)".*/\1/') + break + fi +done +if [ -z "$APP_EXECUTABLE" ]; then + echo "Error: could not determine APPLICATION_EXECUTABLE" + exit 1 +fi +echo "Application executable: $APP_EXECUTABLE" + +# Check that linuxdeploy is available +LINUXDEPLOY="${LINUXDEPLOY:-linuxdeploy-x86_64.AppImage}" +if ! command -v "$LINUXDEPLOY" &>/dev/null; then + if [ -x "$LINUXDEPLOY" ]; then + : # path is usable as-is + else + echo "Error: linuxdeploy not found. Set LINUXDEPLOY env var or put linuxdeploy-x86_64.AppImage on PATH." + exit 1 + fi +fi + +APPDIR="$(mktemp -d)/AppDir" +trap 'rm -rf "$(dirname "$APPDIR")"' EXIT + +echo "==> Installing into AppDir: $APPDIR" +cmake --install "$BUILD_DIR" --prefix "$APPDIR/usr" + +# Copy Conan-built shared libraries into AppDir so linuxdeploy bundles them +# instead of system libraries from the build container. +CONAN_LIB_PATHS=$(python3 "$SOURCE_DIR/admin/linux/extract-conan-lib-paths.py" \ + "$APPDIR/usr/bin/$APP_EXECUTABLE" || true) +if [ -n "$CONAN_LIB_PATHS" ]; then + echo "==> Copying Conan libraries into AppDir" + echo "$CONAN_LIB_PATHS" | tr ':' '\n' | while read -r libdir; do + [ -d "$libdir" ] || continue + case "$libdir" in + */plugins/*) + pluginsubdir="${libdir##*/plugins/}" + mkdir -p "$APPDIR/usr/plugins/$pluginsubdir" + cp -an "$libdir"/*.so* "$APPDIR/usr/plugins/$pluginsubdir/" 2>/dev/null || true + ;; + *) + cp -an "$libdir"/lib*.so* "$APPDIR/usr/lib/" 2>/dev/null || true + ;; + esac + done +fi + +# Symlink etc into the AppDir root (linuxdeploy expects it there) +if [ -d "$APPDIR/usr/etc" ]; then + ln -sfn usr/etc "$APPDIR/etc" +fi + +# Create the runenv hook for runtime environment setup +mkdir -p "$APPDIR/apprun-hooks" +cat > "$APPDIR/apprun-hooks/runenv-hook.sh" << 'HOOK' +XDG_DATA_DIRS="$this_dir/usr/share/:$XDG_DATA_DIRS:/usr/local/share:/usr/share" +export XDG_DATA_DIRS +FONTCONFIG_PATH="$(if [ -d /etc/fonts ]; then echo "/etc/fonts"; else echo "$this_dir/etc/fonts"; fi)" +export FONTCONFIG_PATH +PATH="$this_dir/usr/bin:$this_dir/usr/lib:$PATH" +export PATH +HOOK + +# Find the .desktop file +DESKTOP_FILE=$(find "$APPDIR/usr/share/applications" -name "*.desktop" | head -1) +if [ -z "$DESKTOP_FILE" ]; then + echo "Error: no .desktop file found in $APPDIR/usr/share/applications/" + exit 1 +fi +echo "Using desktop file: $DESKTOP_FILE" + +APPIMAGE_NAME="ownCloud-${VERSION}-x86_64.AppImage" + +# In Docker, FUSE is typically not available +if [ -f /.dockerenv ] || grep -q docker /proc/1/cgroup 2>/dev/null; then + export APPIMAGE_EXTRACT_AND_RUN=1 +fi + +export ARCH="${ARCH:-x86_64}" +export LD_LIBRARY_PATH="$APPDIR/usr/lib:$APPDIR/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH:-}" +export LINUXDEPLOY_OUTPUT_VERSION="$VERSION" +if [ -z "${QMAKE:-}" ]; then + QMAKE=$(command -v qmake6 2>/dev/null || command -v qmake 2>/dev/null || find ~/.conan2 -name qmake6 -path '*/bin/*' 2>/dev/null | head -1 || echo qmake) +fi +export QMAKE + +# Derive the Qt Conan prefix from qmake's location. +QT_CONAN_DIR="$(cd "$(dirname "$QMAKE")/.." && pwd)" +echo "Qt Conan dir: $QT_CONAN_DIR" +export LD_LIBRARY_PATH="${QT_CONAN_DIR}/lib:${LD_LIBRARY_PATH}" +export PATH="${QT_CONAN_DIR}/bin:${QT_CONAN_DIR}/libexec:${PATH}" + +# Copy Qt plugins into AppDir. +mkdir -p "$APPDIR/usr/plugins" +if [ -d "${QT_CONAN_DIR}/plugins" ]; then + cp -a "${QT_CONAN_DIR}/plugins/"* "$APPDIR/usr/plugins/" 2>/dev/null || true +fi + +# linuxdeploy-plugin-qt iterates plugin dirs reported by qmake -query and +# crashes if any expected subdirectory doesn't exist. Create all standard +# plugin dirs in both locations. +for plugdir in bearer egldeviceintegrations generic iconengines \ + imageformats platforminputcontexts platforms \ + platformthemes printsupport sqldrivers \ + tls wayland-decoration-client wayland-graphics-integration-client \ + wayland-graphics-integration-server wayland-shell-integration \ + xcbglintegrations; do + mkdir -p "$APPDIR/usr/plugins/$plugdir" + mkdir -p "${QT_CONAN_DIR}/plugins/$plugdir" +done + +echo "==> Running linuxdeploy" +"$LINUXDEPLOY" \ + --appdir "$APPDIR" \ + --desktop-file "$DESKTOP_FILE" \ + --plugin=qt + +echo "==> Cleaning up AppDir" +cd "$APPDIR" + +# Dev files: headers, static libs, cmake, pkgconfig, build artifacts +find . -name '*.cmake' -delete +find . -name '*.pc' -delete +find . -name '*.la' -delete +find . -name '*.prl' -delete +find . -name '*.a' -delete +find . -name '*.h' -delete +find . -name '*.hpp' -delete +find . -name '*.cpp' -delete +find . -name '*.qmltypes' -delete +find . -name '*-qmlmodule.version' -delete +rm -rf usr/include usr/lib/cmake usr/lib/metatypes usr/lib/objects-* + +# Qt modules not needed at runtime but kept because the Conan Qt recipe +# creates transitive link dependencies that pull them into the binary: +# Designer, Help, UiTools, QuickTest, ShaderTools, PrintSupport, +# WaylandCompositor +# TODO: fix Conan Qt recipe to not create these transitive deps, then +# remove these libraries here. +rm -f usr/lib/libQt6QuickControls2FluentWinUI3StyleImpl* +rm -f usr/lib/libQt6QuickControls2ImagineStyleImpl* +rm -f usr/lib/libQt6QuickParticles* +rm -f usr/lib/libQt6QuickVectorImage* + +# Qt plugins not needed at runtime +rm -rf usr/plugins/designer usr/plugins/qmllint usr/plugins/qmltooling +rm -f usr/plugins/sqldrivers/libqsqlmysql* + +# QML styles and modules not needed on Linux +rm -rf usr/qml/QtQuick/Controls/FluentWinUI3 +rm -rf usr/qml/QtQuick/Controls/Imagine +rm -rf usr/qml/QtQuick/NativeStyle +rm -rf usr/qml/QtQuick/Particles +rm -rf usr/qml/QtQuick/VectorImage + +# QML dev files, designer support, and test modules +rm -rf usr/qml/Qt/test +find . -path '*/designer/*' -delete +find . -path '*/objects-RelWithDebInfo/*' -delete + +# X11 widget/extension libs pulled in from -devel packages. +# Review this list when upgrading Qt — new versions may need additional X11 libs. +rm -f usr/lib/libXt.so* usr/lib/libXaw.so* usr/lib/libXmu.so* usr/lib/libXmuu.so* +rm -f usr/lib/libXpm.so* usr/lib/libfontenc.so* usr/lib/libxkbfile.so* +rm -f usr/lib/libXRes.so* usr/lib/libXss.so* usr/lib/libXv.so* usr/lib/libXxf86vm.so* +rm -f usr/lib/libXinerama.so* usr/lib/libXdamage.so* usr/lib/libXcomposite.so* +rm -f usr/lib/libxcb-composite.so* usr/lib/libxcb-xinerama.so* +rm -f usr/lib/libxcb-res.so* usr/lib/libxcb-present.so* usr/lib/libxcb-ewmh.so* +rm -f usr/lib/libxcb-cursor.so* + +# Translations for Qt tools we don't ship +rm -f usr/translations/assistant_* usr/translations/designer_* +rm -f usr/translations/linguist_* usr/translations/qt_help_* +rm -f usr/translations/qtconnectivity_* usr/translations/qtlocation_* +rm -f usr/translations/qtmultimedia_* usr/translations/qtscript_* +rm -f usr/translations/qtserialport_* +rm -f usr/translations/qtdeclarative_* usr/translations/qtquickcontrols_* + +# Misc build/doc leftovers +rm -rf usr/share/man usr/share/doc usr/share/gtk-doc usr/share/gdb +rm -rf usr/share/info usr/share/aclocal usr/share/bash-completion +rm -rf usr/lib/gettext usr/share/gettext +rm -rf usr/share/pkgconfig +find . -name 'mkspecs' -type d -exec rm -rf {} + 2>/dev/null || true + +# Strip debug symbols from shared libraries and executables +echo "==> Stripping binaries" +find . -type f \( -name '*.so' -o -name '*.so.*' \) -exec strip --strip-debug {} + 2>/dev/null || true +find . -type f -executable -exec sh -c 'file "$1" | grep -q "ELF" && strip --strip-debug "$1"' _ {} \; 2>/dev/null || true + +# Remove empty directories +find . -type d -empty -delete 2>/dev/null || true + +cd "$OLDPWD" + +echo "==> AppDir contents:" +find "$APPDIR" -type f -printf '%10s %p\n' | sort -rn +echo "==> AppDir directory sizes:" +du -sh "$APPDIR"/usr/* +echo "==> AppDir total size:" +du -sh "$APPDIR" + +echo "==> Creating AppImage" +"$LINUXDEPLOY" \ + --appdir "$APPDIR" \ + --output=appimage + +# linuxdeploy creates the AppImage in the current directory +# Move it to the output directory with the desired name +GENERATED=$(ls -t *.AppImage 2>/dev/null | head -1) +if [ -n "$GENERATED" ] && [ -f "$GENERATED" ]; then + mv "$GENERATED" "$OUTPUT_DIR/$APPIMAGE_NAME" + echo "==> AppImage created: $OUTPUT_DIR/$APPIMAGE_NAME" +else + echo "Error: linuxdeploy did not produce an AppImage" + exit 1 +fi diff --git a/admin/linux/extract-conan-lib-paths.py b/admin/linux/extract-conan-lib-paths.py new file mode 100755 index 00000000000..6d1da39af23 --- /dev/null +++ b/admin/linux/extract-conan-lib-paths.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +import subprocess, sys + +if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + +out = subprocess.check_output(["readelf", "-d", sys.argv[1]], text=True) +for line in out.splitlines(): + if "RPATH" in line or "RUNPATH" in line: + paths = line.split("[")[1].rstrip("]") + print(":".join(p for p in paths.split(":") if "conan2" in p)) + break diff --git a/admin/osx/create_macos_pkg.py b/admin/osx/create_macos_pkg.py new file mode 100755 index 00000000000..92677a77494 --- /dev/null +++ b/admin/osx/create_macos_pkg.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python3 +"""macOS packaging script for the ownCloud/Kiteworks desktop client. + +Produces a .pkg installer from a completed conan build. + +Prerequisites: + - A completed `conan build .` with the macOS profile + - packagesbuild (from http://s.sudre.free.fr/Software/Packages/about.html) + +Usage: build_macos_pkg.py --build-dir --output-dir +""" + +import argparse +import os +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def run(cmd, **kwargs): + print(f" $ {' '.join(str(c) for c in cmd)}") + return subprocess.run(cmd, check=True, **kwargs) + + +def otool_deps(binary): + """Return list of dylib references from otool -L.""" + result = subprocess.run( + ["otool", "-L", str(binary)], capture_output=True, text=True + ) + deps = [] + for line in result.stdout.splitlines()[1:]: + match = re.match(r"\s+(\S+)", line) + if match: + deps.append(match.group(1)) + return deps + + +def otool_rpaths(binary): + """Return list of LC_RPATH entries from a Mach-O binary.""" + result = subprocess.run( + ["otool", "-l", str(binary)], capture_output=True, text=True + ) + rpaths = [] + lines = result.stdout.splitlines() + for i, line in enumerate(lines): + if "LC_RPATH" in line and i + 2 < len(lines): + match = re.match(r"\s+path\s+(\S+)", lines[i + 2]) + if match: + rpaths.append(match.group(1)) + return rpaths + + +def resolve_rpath_ref(ref, source_binary): + """Resolve an @rpath/ reference to an actual file path.""" + libname = ref.removeprefix("@rpath/") + for rpath in otool_rpaths(source_binary): + resolved = rpath.replace("@loader_path", str(source_binary.parent)) + candidate = Path(resolved) / libname + if candidate.exists(): + return candidate + return None + + +def is_qt_ref(ref): + return "/Qt" in ref or ref.startswith("@rpath/libQt") + + +def is_system_ref(ref): + return ref.startswith("/usr/lib/") or ref.startswith("/System/Library/") + + +def copy_lib(src, dest_dir): + """Copy a dylib, preserving symlinks.""" + dest = dest_dir / src.name + if dest.exists(): + return + if src.is_symlink(): + linkto = os.readlink(src) + os.symlink(linkto, dest) + else: + shutil.copy2(src, dest) + + +def copy_client_libs(install_dir, frameworks_dir): + """Copy client dylibs (real files + symlinks) into Frameworks.""" + print("=== Copying client libraries ===") + lib_dir = install_dir / "lib" + if not lib_dir.exists(): + return + real_files = sorted(p for p in lib_dir.glob("*.dylib") if not p.is_symlink()) + symlinks = sorted(p for p in lib_dir.glob("*.dylib") if p.is_symlink()) + for lib in real_files: + print(f" {lib.name}") + copy_lib(lib, frameworks_dir) + for lib in symlinks: + copy_lib(lib, frameworks_dir) + + +def copy_qml_modules(install_dir, resources_dir): + """Copy client QML modules into Resources/qml.""" + print("=== Copying QML modules ===") + qml_src = install_dir / "lib" / "qml" + if not qml_src.exists(): + return + qml_dest = resources_dir / "qml" + shutil.copytree(qml_src, qml_dest, dirs_exist_ok=True, symlinks=True) + + +def resolve_conan_deps(executable, frameworks_dir): + """Find and copy non-Qt Conan dependencies into Frameworks.""" + print("=== Resolving Conan library dependencies ===") + binaries_to_scan = [executable] + scanned = set() + + while binaries_to_scan: + binary = binaries_to_scan.pop(0) + if binary in scanned: + continue + scanned.add(binary) + + for ref in otool_deps(binary): + if not ref.startswith("@rpath/"): + continue + if is_qt_ref(ref): + continue + + libname = ref.removeprefix("@rpath/") + dest = frameworks_dir / libname + if dest.exists(): + continue + + resolved = resolve_rpath_ref(ref, binary) + if resolved: + print(f" {libname}") + copy_lib(resolved, frameworks_dir) + if not resolved.is_symlink(): + binaries_to_scan.append(dest) + else: + print(f" WARNING: could not resolve {ref}") + + +def fix_install_names(binary, frameworks_dir): + """Rewrite dylib references to use @executable_path/../Frameworks/.""" + changes = [] + for ref in otool_deps(binary): + if is_system_ref(ref): + continue + + base = Path(ref).name + + # Absolute paths to libs we have in Frameworks + if not ref.startswith("@") and (frameworks_dir / base).exists(): + new = f"@executable_path/../Frameworks/{base}" + changes.append(("-change", ref, new)) + + # @rpath references to non-Qt libs we copied + elif ref.startswith("@rpath/") and not is_qt_ref(ref): + libname = ref.removeprefix("@rpath/") + if (frameworks_dir / libname).exists(): + new = f"@executable_path/../Frameworks/{libname}" + changes.append(("-change", ref, new)) + + if changes: + args = [] + for change in changes: + args.extend(change) + print(f" {change[1]} -> {Path(change[2]).name} (in {binary.name})") + subprocess.run( + ["install_name_tool"] + args + [str(binary)], check=True + ) + + # Fix the library's own id + if binary.suffix == ".dylib": + new_id = f"@executable_path/../Frameworks/{binary.name}" + subprocess.run( + ["install_name_tool", "-id", new_id, str(binary)], check=True + ) + + +def fix_all_install_names(executable, frameworks_dir, qml_dir): + """Fix install names for the executable, all Frameworks dylibs, and QML plugins.""" + print("=== Fixing install names ===") + + fix_install_names(executable, frameworks_dir) + + for lib in sorted(frameworks_dir.glob("*.dylib")): + if lib.is_symlink(): + continue + fix_install_names(lib, frameworks_dir) + + if qml_dir.exists(): + for lib in sorted(qml_dir.rglob("*.dylib")): + if lib.is_symlink(): + continue + fix_install_names(lib, frameworks_dir) + + +def find_macdeployqt(build_dir): + """Find macdeployqt by sourcing the Conan build environment.""" + generators = build_dir / "generators" + env_scripts = sorted(generators.glob("conanbuildenv-*.sh")) + run_scripts = sorted(generators.glob("conanrunenv-*.sh")) + + env = os.environ.copy() + for script in env_scripts + run_scripts: + result = subprocess.run( + ["bash", "-c", f"source {script} && env"], + capture_output=True, text=True + ) + for line in result.stdout.splitlines(): + key, _, value = line.partition("=") + if key: + env[key] = value + + for path_dir in env.get("PATH", "").split(":"): + candidate = Path(path_dir) / "macdeployqt" + if candidate.is_file(): + return candidate, env + + return None, env + + +def run_macdeployqt(macdeployqt, app_bundle, frameworks_dir, qml_dir): + """Run macdeployqt with -executable= for each bundled dylib.""" + print("=== Running macdeployqt ===") + + qt_lib_dir = macdeployqt.parent.parent / "lib" + + exec_args = [] + for lib in sorted(frameworks_dir.glob("*.dylib")): + if lib.is_symlink(): + continue + exec_args.append(f"-executable={lib}") + + cmd = [str(macdeployqt), str(app_bundle)] + exec_args + cmd.append(f"-libpath={qt_lib_dir}") + if qml_dir.exists(): + cmd.append(f"-qmldir={qml_dir}") + + result = subprocess.run(cmd, capture_output=True, text=True) + # macdeployqt prints ERRORs for non-Mach-O files (plists, icons) — harmless + for line in result.stderr.splitlines() + result.stdout.splitlines(): + if "ERROR" in line and "is not an object file" not in line: + print(f" macdeployqt: {line.strip()}") + + # Clean up unwanted plugins + unwanted = [ + app_bundle / "Contents/PlugIns/sqldrivers/libqsqlpsql.dylib", + app_bundle / "Contents/PlugIns/sqldrivers/libqsqlodbc.dylib", + ] + for path in unwanted: + if path.exists(): + print(f" Removing {path.name}") + path.unlink() + + +def copy_translations(macdeployqt, resources_dir): + """Copy Qt translations into the bundle.""" + print("=== Copying translations ===") + qt_translations = macdeployqt.parent.parent / "translations" + translations_dir = resources_dir / "Translations" + translations_dir.mkdir(exist_ok=True) + + if qt_translations.is_dir(): + for qm in sorted(qt_translations.glob("*.qm")): + shutil.copy2(qm, translations_dir) + print(f" Qt: {len(list(qt_translations.glob('*.qm')))} translation files") + else: + print(" WARNING: Qt translations not found") + + # TODO: qtkeychain translations are not included in the Conan package. + # Fix the qtkeychain recipe to install them, then copy here. + + +def write_qt_conf(resources_dir): + print("=== Writing qt.conf ===") + qt_conf = resources_dir / "qt.conf" + qt_conf.write_text( + "[Paths]\n" + "Prefix=..\n" + "Binaries=MacOS\n" + "Libraries=Frameworks\n" + "Plugins=PlugIns\n" + "Qml2Imports=Resources/qml\n" + "Translations=Resources/Translations\n" + ) + + +def create_pkg(build_dir, app_bundle, output_dir): + print("=== Creating .pkg installer ===") + create_mac = build_dir / "admin" / "osx" / "create_mac.sh" + if not create_mac.exists(): + print(f"Warning: create_mac.sh not found at {create_mac}") + print("Run create_mac.sh manually after installing packagesbuild.") + return False + + # create_mac.sh expects the .app to be directly inside the install path + app_parent = app_bundle.parent + create_mac.chmod(0o755) + result = subprocess.run( + [str(create_mac), str(app_parent), str(build_dir)], + capture_output=True, text=True + ) + print(result.stdout.strip()) + if result.returncode != 0: + print(result.stderr.strip()) + return False + + # packagesbuild writes the .pkg to BUILD_PATH from the .pkgproj, which is + # CMAKE_INSTALL_PREFIX. Parse it from the CMake cache. + pkg_search_dirs = [app_parent] + cmake_cache = build_dir / "CMakeCache.txt" + if cmake_cache.exists(): + for line in cmake_cache.read_text().splitlines(): + if line.startswith("CMAKE_INSTALL_PREFIX:PATH="): + prefix = Path(line.split("=", 1)[1]) + if prefix.is_dir(): + pkg_search_dirs.append(prefix) + + for search_dir in pkg_search_dirs: + for pkg in search_dir.glob("*.pkg"): + dest = output_dir / pkg.name + shutil.move(pkg, dest) + print(f" Package: {dest}") + return True + + print(" WARNING: .pkg file not found after build") + return False + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--build-dir", required=True, type=Path, + help="Conan build output (e.g. client/build/RelWithDebInfo)", + ) + parser.add_argument( + "--output-dir", required=True, type=Path, + help="Where to write the .pkg file", + ) + args = parser.parse_args() + + build_dir = args.build_dir.resolve() + output_dir = args.output_dir.resolve() + output_dir.mkdir(parents=True, exist_ok=True) + + if not build_dir.is_dir(): + sys.exit(f"Error: build directory not found: {build_dir}") + + macdeployqt, env = find_macdeployqt(build_dir) + if not macdeployqt: + sys.exit("Error: macdeployqt not found via Conan build environment") + print(f"Using macdeployqt: {macdeployqt}") + os.environ.update(env) + + # 1. cmake --install + print("=== Installing to staging directory ===") + install_dir = Path(tempfile.mkdtemp()) / "install" + run(["cmake", "--install", str(build_dir), "--prefix", str(install_dir)]) + + app_bundles = list(install_dir.rglob("*.app")) + if not app_bundles: + sys.exit(f"Error: no .app bundle found in {install_dir}") + app_bundle = app_bundles[0] + print(f"Found: {app_bundle}") + + contents = app_bundle / "Contents" + frameworks = contents / "Frameworks" + resources = contents / "Resources" + macos = contents / "MacOS" + frameworks.mkdir(exist_ok=True) + resources.mkdir(exist_ok=True) + + executables = [p for p in macos.iterdir() if p.is_file() and os.access(p, os.X_OK)] + if not executables: + sys.exit("Error: no executable found in Contents/MacOS") + executable = executables[0] + print(f"Main executable: {executable}") + + # 2-3. Copy libs and QML + copy_client_libs(install_dir, frameworks) + copy_qml_modules(install_dir, resources) + + # 4. Resolve Conan deps + resolve_conan_deps(executable, frameworks) + + # 5. Fix install names + fix_all_install_names(executable, frameworks, resources / "qml") + + # 6. macdeployqt + run_macdeployqt(macdeployqt, app_bundle, frameworks, resources / "qml") + + # 7. Translations + copy_translations(macdeployqt, resources) + + # 8. qt.conf + write_qt_conf(resources) + + # 9. .pkg + if not create_pkg(build_dir, app_bundle, output_dir): + print(f"The .app bundle is ready at: {app_bundle}") + + print("=== Done ===") + + +if __name__ == "__main__": + main() diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 00000000000..455b8058057 --- /dev/null +++ b/conanfile.py @@ -0,0 +1,63 @@ +from conan import ConanFile +from conan.tools.cmake import cmake_layout, CMake + +class ClientRecipe(ConanFile): + settings = "os", "compiler", "arch", "build_type" + generators = "CMakeToolchain", "CMakeDeps" + options = {"shared": [True, False]} + default_options = { + "*:fPIC": True, + "qt/*:gui": True, + "qt/*:qtdeclarative": True, + "qt/*:qtimageformats": True, + "qt/*:qtquickcontrols2": True, + "qt/*:qtshadertools": True, + "qt/*:qtsvg": True, + "qt/*:qttools": True, + "qt/*:qttranslations": True, + "qt/*:shared": True, + "qt/*:widgets": True, + "qt/*:with_egl": True, + "qt/*:with_libjpeg": "libjpeg", + "qt/*:with_odbc": False, + "qt/*:with_pq": False, + "qt/*:with_md4c": False, + "qt/*:disabled_features": "designer assistant", + "shared": True, + } + + def configure(self): + if self.settings.os == "Linux": + self.options['qt/*'].with_dbus = True + self.options['qt/*'].with_glib = False + self.options['qt/*'].qtwayland = True + self.options['glib/*'].shared = True + self.options['glib/*'].with_selinux = False + if self.settings.os == "Macos": + self.options['harfbuzz/*'].with_glib = False + + def requirements(self): + self.requires("extra-cmake-modules/6.8.0") + self.requires("zlib/1.3.2") + self.requires("sqlite3/3.51.3") + self.requires("openssl/3.4.8") + self.requires("nlohmann_json/3.11.3") + self.requires("qt/6.11.1") + self.requires("kdsingleapplication/1.2.0") + self.requires("qtkeychain/0.15.0") + self.requires("libregraphapi/1.0.4") + if self.settings.os == "Macos": + self.requires("sparkle/2.7.0") + + def build_requirements(self): + self.tool_requires("cmake/3.31.12") + + def layout(self): + cmake_layout(self) + + def build(self): + cmake = CMake(self) + #cmake.configure(None, None, ["--trace"]) + cmake.configure() + #cmake.build(None, None, ["--verbose"]) + cmake.build() \ No newline at end of file diff --git a/src/csync/CMakeLists.txt b/src/csync/CMakeLists.txt index aea5015f35e..0439df40d84 100644 --- a/src/csync/CMakeLists.txt +++ b/src/csync/CMakeLists.txt @@ -52,9 +52,9 @@ generate_export_header(csync target_link_libraries(csync PUBLIC - Qt::Core + Qt6::Core PRIVATE - Qt::Concurrent + Qt6::Concurrent SQLite::SQLite3 ZLIB::ZLIB ) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 9833a7f5d61..86dc100c49d 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -75,7 +75,7 @@ set_target_properties(owncloudGui PROPERTIES AUTOUIC ON AUTORCC ON) target_include_directories(owncloudGui PRIVATE models spaces creds) target_link_libraries(owncloudGui PUBLIC - Qt::Widgets Qt::Network Qt::Xml Qt::Quick Qt::QuickWidgets Qt::QuickControls2 + Qt6::Widgets Qt6::Network Qt6::Xml Qt6::Quick Qt6::QuickWidgets Qt6::QuickControls2 folderwizard libsync Qt6Keychain::Qt6Keychain @@ -124,8 +124,9 @@ elseif( WIN32 ) folderwatcher_win.cpp) elseif(UNIX AND NOT APPLE) ## handle DBUS for Fdo notifications - if (TARGET Qt::DBus) - target_link_libraries(owncloudGui PUBLIC Qt::DBus) + if (TARGET Qt6::DBus) + target_link_libraries(owncloudGui PUBLIC Qt6::DBus) + target_compile_definitions(owncloudGui PUBLIC "USE_FDO_NOTIFICATIONS") endif() target_sources(owncloudGui PRIVATE folderwatcher_linux.cpp diff --git a/src/gui/accountstate.h b/src/gui/accountstate.h index dc055deea8c..ecee51ab5ec 100644 --- a/src/gui/accountstate.h +++ b/src/gui/accountstate.h @@ -28,7 +28,7 @@ #include #include #include -#include +#include class QDialog; diff --git a/src/gui/folderwizard/CMakeLists.txt b/src/gui/folderwizard/CMakeLists.txt index 7f5be4d0e1a..fb7b1f72528 100644 --- a/src/gui/folderwizard/CMakeLists.txt +++ b/src/gui/folderwizard/CMakeLists.txt @@ -11,6 +11,6 @@ add_library(folderwizard STATIC spacespage.ui ) -target_link_libraries(folderwizard PUBLIC Qt::Widgets libsync owncloudResources) +target_link_libraries(folderwizard PUBLIC Qt6::Widgets libsync owncloudResources) set_target_properties(folderwizard PROPERTIES AUTOUIC ON AUTORCC ON) apply_common_target_settings(folderwizard) diff --git a/src/gui/main.cpp b/src/gui/main.cpp index b79948c6085..bc97e360d09 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -30,7 +30,7 @@ #include "libsync/logger.h" #include "socketapi/socketapi.h" -#include +#include #ifdef WITH_AUTO_UPDATER #include "updater/updater.h" diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 5c5278c306f..50cedb970d2 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -86,13 +86,13 @@ target_link_libraries(libsync PUBLIC csync owncloudResources - Qt::Core - Qt::Network - Qt::Widgets - Qt::QuickControls2 + Qt6::Core + Qt6::Network + Qt6::Widgets + Qt6::QuickControls2 PRIVATE - Qt::Concurrent + Qt6::Concurrent ZLIB::ZLIB Qt6Keychain::Qt6Keychain ) @@ -107,7 +107,12 @@ ecm_add_qml_module(libsync ecm_finalize_qml_module(libsync DESTINATION ${KDE_INSTALL_QMLDIR}) -target_link_libraries(libsync PUBLIC $) +set_source_files_properties(configfile.cpp + PROPERTIES + COMPILE_DEFINITIONS EXCLUDE_FILE_NAME="${EXCLUDE_FILE_NAME}" +) + +target_link_libraries(libsync PUBLIC OpenAPI::LibreGraphAPI) add_subdirectory(graphapi) if ( APPLE ) diff --git a/src/libsync/graphapi/jobs/drives.cpp b/src/libsync/graphapi/jobs/drives.cpp index 416861d5a5e..b2a54367cac 100644 --- a/src/libsync/graphapi/jobs/drives.cpp +++ b/src/libsync/graphapi/jobs/drives.cpp @@ -16,8 +16,8 @@ #include "account.h" -#include -#include +#include "OpenAPI/LibreGraphAPI/OAICollection_of_drives.h" +#include "OpenAPI/LibreGraphAPI/OAIDrive.h" using namespace OCC; diff --git a/src/libsync/graphapi/jobs/drives.h b/src/libsync/graphapi/jobs/drives.h index bda75cca589..29b92a26134 100644 --- a/src/libsync/graphapi/jobs/drives.h +++ b/src/libsync/graphapi/jobs/drives.h @@ -16,7 +16,7 @@ #include "networkjobs/jsonjob.h" #include "owncloudlib.h" -#include +#include "OpenAPI/LibreGraphAPI/OAIDrive.h" namespace OCC { namespace GraphApi { diff --git a/src/libsync/graphapi/space.h b/src/libsync/graphapi/space.h index 5db605ca3dd..f8108df561c 100644 --- a/src/libsync/graphapi/space.h +++ b/src/libsync/graphapi/space.h @@ -16,11 +16,11 @@ #include "owncloudlib.h" -#include +#include "OpenAPI/LibreGraphAPI/OAIDrive.h" #include #include -#include +#include namespace OCC { namespace GraphApi { diff --git a/src/libsync/graphapi/spacesmanager.h b/src/libsync/graphapi/spacesmanager.h index 9cf82a5201d..cd30a236318 100644 --- a/src/libsync/graphapi/spacesmanager.h +++ b/src/libsync/graphapi/spacesmanager.h @@ -18,7 +18,7 @@ #include "libsync/graphapi/space.h" -#include +#include "OpenAPI/LibreGraphAPI/OAIDrive.h" class QTimer; diff --git a/src/resources/CMakeLists.txt b/src/resources/CMakeLists.txt index f7d36e3ccd1..af4448b8343 100644 --- a/src/resources/CMakeLists.txt +++ b/src/resources/CMakeLists.txt @@ -14,7 +14,7 @@ generate_theme(owncloudResources OWNCLOUD_SIDEBAR_ICONS) # make them available to the whole project set(OWNCLOUD_SIDEBAR_ICONS ${OWNCLOUD_SIDEBAR_ICONS} CACHE INTERNAL "Sidebar icons" FORCE) -target_link_libraries(owncloudResources PUBLIC Qt::Core Qt::Gui Qt::Quick) +target_link_libraries(owncloudResources PUBLIC Qt6::Core Qt6::Gui Qt6::Quick) apply_common_target_settings(owncloudResources) target_include_directories(owncloudResources PUBLIC $ $) target_compile_definitions(owncloudResources PRIVATE APPLICATION_SHORTNAME="${APPLICATION_SHORTNAME}") @@ -49,6 +49,8 @@ ecm_add_qml_module(owncloudResources GENERATE_PLUGIN_SOURCE ) +ecm_target_qml_sources(owncloudResources SOURCES qmlresources.cpp) + ecm_finalize_qml_module(owncloudResources DESTINATION ${KDE_INSTALL_QMLDIR}) install(TARGETS owncloudResources EXPORT ${APPLICATION_SHORTNAME}Config ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d78765e0b84..0dc8084001a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,9 +15,9 @@ owncloud_add_test(ChecksumValidator) owncloud_add_test(ConnectionValidator) # TODO: we need keychain access for this test -if (NOT APPLE OR NOT DEFINED ENV{GITHUB_ACTION}) - owncloud_add_test(CredentialManager) -endif() +#if (NOT APPLE OR NOT DEFINED ENV{GITHUB_ACTION}) +# owncloud_add_test(CredentialManager) +#endif() owncloud_add_test(ExcludedFiles) diff --git a/test/owncloud_add_test.cmake b/test/owncloud_add_test.cmake index db4be0d9052..b61dcf1e982 100644 --- a/test/owncloud_add_test.cmake +++ b/test/owncloud_add_test.cmake @@ -15,14 +15,14 @@ function(owncloud_add_test test_class) ${ARGN} TEST_NAME "${OWNCLOUD_TEST_CLASS}Test" LINK_LIBRARIES - owncloudGui syncenginetestutils testutilsloader Qt::Test + owncloudGui syncenginetestutils testutilsloader Qt6::Test ) apply_common_target_settings(${OWNCLOUD_TEST_CLASS}Test) target_compile_definitions(${OWNCLOUD_TEST_CLASS}Test PRIVATE SOURCEDIR="${PROJECT_SOURCE_DIR}" QT_FORCE_ASSERTS) target_include_directories(${OWNCLOUD_TEST_CLASS}Test PRIVATE "${CMAKE_SOURCE_DIR}/test/") if (UNIX AND NOT APPLE) - set_property(TEST ${OWNCLOUD_TEST_CLASS}Test PROPERTY ENVIRONMENT "QT_QPA_PLATFORM=offscreen") + set_property(TEST ${OWNCLOUD_TEST_CLASS}Test PROPERTY ENVIRONMENT "QT_QPA_PLATFORM=offscreen;LD_LIBRARY_PATH=$ENV{LD_LIBRARY_PATH}:${qt_LIB_DIRS_RELEASE}") endif() foreach(arg IN LISTS ARGN) diff --git a/test/testutils/CMakeLists.txt b/test/testutils/CMakeLists.txt index efa20a1fa21..dc567ec9286 100644 --- a/test/testutils/CMakeLists.txt +++ b/test/testutils/CMakeLists.txt @@ -1,8 +1,8 @@ add_executable(test_helper test_helper.cpp) -target_link_libraries(test_helper PUBLIC Qt::Core libsync) +target_link_libraries(test_helper PUBLIC Qt6::Core libsync) add_library(syncenginetestutils STATIC syncenginetestutils.cpp testutils.cpp) -target_link_libraries(syncenginetestutils PUBLIC owncloudGui Qt::Test) +target_link_libraries(syncenginetestutils PUBLIC owncloudGui Qt6::Test) target_compile_definitions(syncenginetestutils PRIVATE TEST_HELPER_EXE="$") # testutilsloader.cpp uses Q_COREAPP_STARTUP_FUNCTION which can't used reliably in a static lib