From e87ef819a4b636ce2b90cb90400d35c14e66a749 Mon Sep 17 00:00:00 2001
From: Christophe Guillon <christophe.guillon@inria.fr>
Date: Mon, 8 Jul 2024 16:19:56 +0200
Subject: [PATCH] [Setup] Use symlinks in editable mode post install

In editable mode, post installation of files use symlinks
instead of copy.
This allow to the either rebuild with pip or directly with the
build backend, for instance:
- make -C build install
- ninja -C build install # with Ninja backend
---
 README.md |  8 ++++++++
 setup.py  | 23 ++++++++++++++++-------
 2 files changed, 24 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index 6ff9ec438..246472888 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,14 @@ After changes in a python file, no action is required, for changes in .cpp/.h fi
 pip install --no-build-isolation -v -e .
 ```
 
+Or simply use the build backend (generated files in the `build/` directory have been symlinked
+during the first editable install):
+```bash
+make -C build install -j $(nproc)
+# or for instance with ninja backend
+ninja -C build install
+```
+
 In this mode, the C++ compilation build system is located in the local `build/` directory.
 
 Refer to the doc string in `setup.py`for more details on editable mode.
diff --git a/setup.py b/setup.py
index e88e10ef5..110118531 100644
--- a/setup.py
+++ b/setup.py
@@ -47,7 +47,7 @@ Development Status :: 2 - Pre-Alpha
 """
 
 import shutil
-import pathlib
+from pathlib import Path
 import subprocess
 import multiprocessing
 
@@ -57,7 +57,7 @@ from setuptools import setup, Extension
 from setuptools import find_packages
 from setuptools.command.build_ext import build_ext
 
-SETUP_DIR = pathlib.Path(__file__).parent
+SETUP_DIR = Path(__file__).parent
 
 def parse_requirements():
     with open(SETUP_DIR / "requirements.txt", "r") as inf:
@@ -90,19 +90,28 @@ class CMakeBuild(build_ext):
             else:
                 self.editable_mode = False
 
+    def _post_install_copy(self, fname, dst_dir):
+        Path(dst_dir).mkdir(parents=True, exist_ok=True)
+        dst_file = Path(dst_dir) / Path(fname).name
+        if not self.editable_mode:
+            shutil.copy(str(fname), str(dst_file))
+        else:
+            dst_file.unlink(missing_ok=True)
+            os.symlink(str(fname), str(dst_file))
+
     def run(self):
         # This lists the number of processors available on the machine
         # The compilation will use half of them
         # Note that for ninja backend this is not necessary
         max_jobs = str(ceil(multiprocessing.cpu_count() / 2))
 
-        cwd = pathlib.Path().absolute()
+        cwd = Path().absolute()
 
         build_temp = cwd / "build"
         if not build_temp.exists():
             build_temp.mkdir(parents=True, exist_ok=True)
 
-        build_lib = pathlib.Path(self.build_lib)
+        build_lib = Path(self.build_lib)
         if not build_lib.exists():
             build_lib.mkdir(parents=True, exist_ok=True)
 
@@ -135,11 +144,11 @@ class CMakeBuild(build_ext):
                 if ((file.endswith('.so') or file.endswith('.pyd')) and
                     root != str(aidge_package) and
                     not root.startswith(str(build_lib))):
-                    currentFile = pathlib.Path(root) / file
-                    shutil.copy(str(currentFile), str(aidge_package))
+                    currentFile = Path(root) / file
+                    self._post_install_copy(currentFile, aidge_package)
 
         # Copy version.txt in aidge_package
-        shutil.copy(str(SETUP_DIR / "version.txt"), str(aidge_package))
+        self._post_install_copy(SETUP_DIR / "version.txt", str(aidge_package))
 
 if __name__ == '__main__':
 
-- 
GitLab