Repository: pdfpc/pdfpc
Pull request #750: refactor(windows): Load resources relative to the executable
Author: login: Nerixyz
Created: 2025-05-01T16:13:23Z | Merged: 2025-05-07T06:55:33Z

Since https://github.com/pdfpc/pdfpc/pull/700 is dead, I'm trying to get at least some parts in.

This PR refactors the resource loading to prioritize files that lie next to the executable on Windows. The following layout is assumed:

```
install_dir/
├─ bin/
│  └─ pdfpc.exe
├─ share/
│  └─ pdfpc/
│     ├─ css/
│     │  └─ notes.css, pdfpc.css
│     └─ icons/
│        └─ *.svg
└─ etc/
   └─ pdfpcrc
```

This is the layout `cmake --install` produces.

Semi-related: This PR adjusts the `install` calls to specify paths relative to the install-prefix. This allows one to set `--prefix` when installing even after the project has been configured.

---

I have added an installer in https://github.com/Nerixyz/pdfpc/tree/feat/win-installer which I would PR if this gets merged.


Diff:
diff --git a/css/CMakeLists.txt b/css/CMakeLists.txt
index dae90fa6..40ef35f8 100644
--- a/css/CMakeLists.txt
+++ b/css/CMakeLists.txt
@@ -1,5 +1,5 @@
 install(FILES
     pdfpc.css notes.css
 DESTINATION
-    ${CMAKE_INSTALL_PREFIX}/share/pdfpc/css
+    share/pdfpc/css
 )
diff --git a/rc/CMakeLists.txt b/rc/CMakeLists.txt
index d0bf4296..7a23c8b5 100644
--- a/rc/CMakeLists.txt
+++ b/rc/CMakeLists.txt
@@ -1,5 +1,5 @@
 install(FILES
     pdfpcrc
 DESTINATION
-    ${CMAKE_INSTALL_FULL_SYSCONFDIR}
+    ${CMAKE_INSTALL_SYSCONFDIR}
 )
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 29fb83d7..4823a362 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -148,6 +148,10 @@ if (CC_HAS_IMPLICIT_FUNCTION_DECLARATION)
     add_definitions("-Wno-implicit-function-declaration")
 endif()
 
+if(WIN32)
+    set(EXTRA_VALA_OPTIONS ${EXTRA_VALA_OPTIONS} -D WIN32)
+endif()
+
 vala_precompile(VALA_C
     ${VALA_SRC}
 PACKAGES
diff --git a/src/classes/renderer/image.vala b/src/classes/renderer/image.vala
index 412d471e..152147d5 100644
--- a/src/classes/renderer/image.vala
+++ b/src/classes/renderer/image.vala
@@ -29,14 +29,7 @@ namespace pdfpc.Renderer {
             int width, int height) {
             // attempt to load from a local path (if the user hasn't installed)
             // if that fails, attempt to load from the global path
-            string load_icon_path;
-            if (Options.no_install) {
-                load_icon_path = Path.build_filename(Paths.SOURCE_PATH, "icons",
-                    filename);
-            } else {
-                load_icon_path = Path.build_filename(Paths.SHARE_PATH, "icons",
-                    filename);
-            }
+            string load_icon_path = Resources.resolve(Path.build_filename("icons", filename));
             File icon_file = File.new_for_path(load_icon_path);
 
             try {
diff --git a/src/classes/resources.vala b/src/classes/resources.vala
new file mode 100644
index 00000000..dde72a51
--- /dev/null
+++ b/src/classes/resources.vala
@@ -0,0 +1,40 @@
+namespace pdfpc {
+    class Resources {
+        // `bin` directory
+        private static string executable_dir = "";
+
+        public static void init(string path) {
+            executable_dir = Path.get_dirname(path);
+        }
+
+        public static string resolve(string resource) {
+#if WIN32
+            var local_path = Path.build_filename(executable_dir, "../share/pdfpc", resource);
+            if (GLib.FileUtils.test(local_path, GLib.FileTest.EXISTS)) {
+                return local_path;
+            }
+#endif
+
+            if (Options.no_install) {
+                return Path.build_filename(Paths.SOURCE_PATH, resource);
+            } else {
+                return Path.build_filename(Paths.SHARE_PATH, resource);
+            }
+        }
+
+        public static string resolve_system_config() {
+#if WIN32
+            var local_path = Path.build_filename(executable_dir, "..", "etc", "pdfpcrc");
+            if (GLib.FileUtils.test(local_path, GLib.FileTest.EXISTS)) {
+                return local_path;
+            }
+#endif
+
+            if (Options.no_install) {
+                return Path.build_filename(Paths.SOURCE_PATH, "rc/pdfpcrc");
+            } else {
+                return Path.build_filename(Paths.CONF_PATH, "pdfpcrc");
+            }
+        }
+    }
+}
diff --git a/src/classes/rest_server.vala b/src/classes/rest_server.vala
index 61490f32..d42686f2 100644
--- a/src/classes/rest_server.vala
+++ b/src/classes/rest_server.vala
@@ -502,13 +502,7 @@ namespace pdfpc {
             if (Options.rest_static_root.has_prefix("/")) {
                 this.static_root = Options.rest_static_root;
             } else {
-                if (Options.no_install) {
-                    this.static_root = Path.build_filename(Paths.SOURCE_PATH,
-                        Options.rest_static_root);
-                } else {
-                    this.static_root = Path.build_filename(Paths.SHARE_PATH,
-                        Options.rest_static_root);
-                }
+                this.static_root = Resources.resolve(Options.rest_static_root);
             }
 
             this.add_handler("/api", api_handler);
diff --git a/src/classes/view/markdown.vala b/src/classes/view/markdown.vala
index ec8f6b82..3e9ff07f 100644
--- a/src/classes/view/markdown.vala
+++ b/src/classes/view/markdown.vala
@@ -28,12 +28,7 @@ namespace pdfpc.View {
         public MarkdownView() {
             this.ucm = this.get_user_content_manager();
 
-            string css_path;
-            if (Options.no_install) {
-                css_path = Path.build_filename(Paths.SOURCE_PATH, "css/notes.css");
-            } else {
-                css_path = Path.build_filename(Paths.SHARE_PATH, "css/notes.css");
-            }
+            string css_path = Resources.resolve("css/notes.css");
 
             try {
                 File css_file = File.new_for_path(css_path);
diff --git a/src/pdfpc.vala b/src/pdfpc.vala
index 9543aaab..5f828304 100644
--- a/src/pdfpc.vala
+++ b/src/pdfpc.vala
@@ -209,12 +209,7 @@ namespace pdfpc {
             Gtk.StyleContext.add_provider_for_screen(Gdk.Display.get_default().get_default_screen(),
                 userProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER);
 
-            string distCssPath;
-            if (Options.no_install) {
-                distCssPath = Path.build_filename(Paths.SOURCE_PATH, "css/pdfpc.css");
-            } else {
-                distCssPath = Path.build_filename(Paths.SHARE_PATH, "css/pdfpc.css");
-            }
+            string distCssPath = Resources.resolve("css/pdfpc.css");
             var legacyUserCssPath = Path.build_filename(GLib.Environment.get_user_config_dir(), "pdfpc.css");
             var userCssPath = Path.build_filename(GLib.Environment.get_user_config_dir(), "pdfpc", "pdfpc.css");
 
@@ -285,14 +280,7 @@ namespace pdfpc {
 
             ConfigFileReader configFileReader = new ConfigFileReader();
 
-            string systemConfig;
-            if (Options.no_install) {
-                systemConfig = Path.build_filename(Paths.SOURCE_PATH,
-                    "rc/pdfpcrc");
-            } else {
-                systemConfig = Path.build_filename(Paths.CONF_PATH,
-                    "pdfpcrc");
-            }
+            string systemConfig = Resources.resolve_system_config();
             configFileReader.readConfig(systemConfig);
 
             // First, try the XDG config directory
@@ -587,6 +575,7 @@ namespace pdfpc {
          * Basic application entry point
          */
         public static int main (string[] args) {
+            Resources.init(args[0]);
             var application = new Application();
             application.run(args);
 


Reviews:
user: login: fnevgeny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T20:05:52Z
user: login: Nerixyz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:12:11Z
user: login: Nerixyz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:12:43Z
user: login: fnevgeny
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-05T20:31:12Z
user: login: AndreasBilke
body: Thank you so much for your effort!

Does your PR needs also adjustments for the installation of man pages? (see `man/CMakeLists.txt` ?)
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T07:09:48Z
user: login: fnevgeny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T08:24:22Z
user: login: Nerixyz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:46:23Z
user: login: fnevgeny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T08:53:08Z
user: login: Nerixyz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:59:14Z
user: login: Nerixyz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T15:08:17Z
user: login: fnevgeny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:58:06Z
user: login: Nerixyz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T16:10:42Z
user: login: fnevgeny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T16:16:27Z
user: login: AndreasBilke
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-07T06:55:10Z

Review comments:
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: fnevgeny
body: Why is this change important/relevant?
created_at: 2025-05-05T20:02:48Z
author_association: MEMBER
diff_hunk: @@ -148,6 +148,10 @@ if (CC_HAS_IMPLICIT_FUNCTION_DECLARATION)
     add_definitions("-Wno-implicit-function-declaration")
 endif()
 
+if(${CMAKE_HOST_WIN32})
path: src/CMakeLists.txt
user: login: fnevgeny
body: Won't it break when cross-compiling?
created_at: 2025-05-05T20:03:47Z
author_association: MEMBER
diff_hunk: @@ -148,6 +148,10 @@ if (CC_HAS_IMPLICIT_FUNCTION_DECLARATION)
     add_definitions("-Wno-implicit-function-declaration")
 endif()
 
+if(${CMAKE_HOST_WIN32})
path: src/CMakeLists.txt
user: login: Nerixyz
body: Good, point, that should be `WIN32`.
created_at: 2025-05-05T20:12:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: Nerixyz
body: It's for CMake 4.0 compatibility. So not directly related, could open another PR for this if desired.
created_at: 2025-05-05T20:12:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: AndreasBilke
body: What parts of your change would be incompatible with cmake 3? And if so: Isn't cmake 3.31 the latest of v3?
created_at: 2025-05-06T07:07:46Z
author_association: MEMBER
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: fnevgeny
body: Indeed, maybe make it 3.99 to be on the safe side (if 4 does introduce some backward incompatibilities)?
created_at: 2025-05-06T08:24:22Z
author_association: MEMBER
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: Nerixyz
body: This PR doesn't introduce any incompatible changes. The syntax is a bit confusing. The second argument to `cmake_minimum_required` sets the [minimum policy version](https://cmake.org/cmake/help/latest/command/cmake_minimum_required.html#policy-version). It basically says: "this is a (policy-)version this project can build with". In CMake, this will set all policies introduced in that version to `NEW`. Since there aren't any policies used (AND CMake 4 doesn't warn about any implicitly used ones), there's no issue bumping this.

I can set it to 3.31.
created_at: 2025-05-06T08:46:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: fnevgeny
body: > This PR doesn't introduce any incompatible changes.

No, I meant CMake4's incompatibilities. The second argument of `cmake_minimum_required` is the *maximal* version that can be used with this project, isn't it? Then we'd need to update the build every time a new CMake-3 is released. Hence I suggest setting it to something that will never be overflown.
created_at: 2025-05-06T08:53:08Z
author_association: MEMBER
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: Nerixyz
body: > The second argument of `cmake_minimum_required` is the _maximal_ version that can be used with this project, isn't it?

Ah no, that's the confusion I meant. It's the ~~minimum~~ maximum policy version ([docs](https://cmake.org/cmake/help/latest/command/cmake_minimum_required.html)), so it only controls breaking changes _up to_ that version.
created_at: 2025-05-06T08:59:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: Nerixyz
body: Done.
created_at: 2025-05-06T15:08:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: fnevgeny
body: > Ah no, that's the confusion I meant. It's the ~minimum~ maximum policy version ([docs](https://cmake.org/cmake/help/latest/command/cmake_minimum_required.html)), so it only controls breaking changes _up to_ that version.

OK, I see - the last argument is about the _policy_ version, not cmake's version. Isn't it a sure thing to introduce incompatibilities? If someone uses the older cmake-3.7, it will behave differently from a newer version supporting some new policies introduced lately, right? I fail to see why it is desired - in general, and in the case of pdfpc specifically.

Perhaps this change should be removed from this PR.
created_at: 2025-05-06T15:58:06Z
author_association: MEMBER
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: Nerixyz
body: CMake will warn when a project relies on a policy that's set to `OLD`. Since that's not the case here, it's fine to bump the policy. In CMake 4, you get the following warning:

```
Compatibility with CMake < 3.10 will be removed from a future version of
CMake.

Update the VERSION argument <min> value.  Or, use the <min>...<max> syntax
to tell CMake that the project requires at least <min> but has been updated
to work with policies introduced by <max> or earlier.
```

But I agree, let's move this to another PR.
created_at: 2025-05-06T16:10:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.7...3.30)
path: CMakeLists.txt
user: login: fnevgeny
body: > In CMake 4, you get the following warning

Ah, I finally got it :)
created_at: 2025-05-06T16:16:27Z
author_association: MEMBER

PR comments:
user: login: Nerixyz
created_at: 2025-05-06T07:56:29Z
body: > Does your PR needs also adjustments for the installation of man pages? 

Ah, I missed them. I'll adjust that when I get back (or if you have a quick fix, feel free to push to my branch).
author_association: CONTRIBUTOR
user: login: Nerixyz
created_at: 2025-05-06T15:08:12Z
body: > Does your PR needs also adjustments for the installation of man pages? (see `man/CMakeLists.txt` ?)

Looks like those are already handled correctly by CMake as they use `CMAKE_INSTALL_MANDIR` instead of `CMAKE_INSTALL_FULL_MANDIR` (see [GNUInstallDirs](https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html)).

Just for completeness:

<details><summary>Full cmake install output on Windows</summary>

`CMAKE_INSTALL_PREFIX` was the default at `C:/Program Files (x86)/pdfpc` and I invoked `cmake --install . --prefix 2install2`.
```
-- Install configuration: "Debug"
-- Installing: F:/Dev/pdfpc/build/2install2/bin/pdfpc.exe
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/blank.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/hidden.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/snow.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/pause.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/saved.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/loaded.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/locked.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/empty.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/highlight.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/pen.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/eraser.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/move.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/settings.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/linewidth.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/spotlight.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/play_cur.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/icons/pause_cur.svg
-- Installing: F:/Dev/pdfpc/build/2install2/share/man/man1/pdfpc.1
-- Installing: F:/Dev/pdfpc/build/2install2/share/man/man5/pdfpcrc.5
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/css/pdfpc.css
-- Installing: F:/Dev/pdfpc/build/2install2/share/pdfpc/css/notes.css
-- Installing: F:/Dev/pdfpc/build/2install2/etc/pdfpcrc
```
</details> 
author_association: CONTRIBUTOR