From 29620fec21b230f9ab5b8714e2c471259af74b96 Mon Sep 17 00:00:00 2001
From: AlQuemist <alquemist@Lyriks>
Date: Tue, 6 Jun 2023 14:54:45 +0200
Subject: [PATCH] Add a script to build BornAgain for multiple platforms and
 different Python versions

---
 devtools/deploy/multipy/build_multipy.py | 205 +++++++++++++++++++++++
 1 file changed, 205 insertions(+)
 create mode 100644 devtools/deploy/multipy/build_multipy.py

diff --git a/devtools/deploy/multipy/build_multipy.py b/devtools/deploy/multipy/build_multipy.py
new file mode 100644
index 00000000000..9664bcea873
--- /dev/null
+++ b/devtools/deploy/multipy/build_multipy.py
@@ -0,0 +1,205 @@
+"""
+Build BornAgain with multiple versions of Python
+"""
+
+import os, platform, re, subprocess as subp
+import re
+from timeit import default_timer as timer
+from collections import namedtuple
+
+# all environmental variables
+ENV = os.environ
+BUILD_DIR = "build"
+
+# separator line
+SEPLINE = "-" * 40
+
+# directories needed for the full build
+BuildDirs = namedtuple("BuildDirectories", ["source", "build", "package", "python"])
+
+class WindowsMake:
+    """ Build on Windows """
+    PLATFORM = "windows"
+    ARCH = "amd64"
+    NPROC = 8
+    # initial path
+    PATH_INI = ENV["PATH"]
+    OPT_DIR = "C:/opt/x64"
+    FFTW3_INCLUDE_DIR = OPT_DIR + "/include"
+    FFTW3_LIB = OPT_DIR + "/lib/libfftw3-3.lib"
+    BOOST_DIR = OPT_DIR + "/boost_current"
+    BOOST_INCLUDE_DIR = BOOST_DIR + "/include"
+    BOOST_LIB_DIR = BOOST_DIR + "/lib"
+    QT_MSVC_DIR = "C:/Qt/6.2.4/msvc2019_64"
+    QTCMake_DIR = QT_MSVC_DIR + "/lib/cmake"
+    PY_PLATFORM_BASE = "C:/opt/multipython/Python"
+    # supported Python platforms: default (3.9), 3.8, 3.10, 3.11
+    SUPPORTED_PY_VERSIONS = ("", "38", "310", "311")
+
+    def configure(dirs:BuildDirs):
+        """
+        Configure the build with a given source and build directory
+        and a given Python platform
+        """
+
+        # NOTE: The command is equivalent to the following shell command:
+        # $ cmake -G "Visual Studio 17 2022" -A x64 -T host=x64 ... \
+        #     -DCMAKE_PREFIX_PATH="optdir;qtcmakedir" ...
+        # Notice that the double-quotation marks should not be included
+        # in the passed arguments.
+        conf_prc = subp.run(
+            ["cmake", "-G", "Visual Studio 17 2022",
+             "-A x64",
+             "-T host=x64",
+             "-DCMAKE_PREFIX_PATH=" + WindowsMake.OPT_DIR + ";" + WindowsMake.QTCMake_DIR,
+             "-DFFTW3_INCLUDE_DIR=" + WindowsMake.FFTW3_INCLUDE_DIR,
+             "-DFFTW3_LIBRARY=" + WindowsMake.FFTW3_LIB,
+             "-DCMAKE_INCLUDE_PATH=" + WindowsMake.OPT_DIR + "/include;"
+             + WindowsMake.BOOST_INCLUDE_DIR,
+             "-DCMAKE_LIBRARY_PATH=" + WindowsMake.OPT_DIR + "/lib;"
+             + WindowsMake.BOOST_LIB_DIR,
+             "-DBA_PY_PACKAGE=ON",
+             "-DBA_PY_PLATFORM=" + dirs.python,
+             "-DCMAKE_C_COMPILER=cl.exe",
+             "-DCMAKE_CXX_COMPILER=cl.exe",
+             "-B", dirs.build, "-S", dirs.source])
+
+        return conf_prc
+
+    def build(dirs:BuildDirs):
+        """ Build in the given build directory """
+        build_prc = subp.run(["cmake", "--build", dirs.build, "--config Release", "--",
+                              "/fl", "/flp:logfile=BornAgainMSBuild.log", "/verbosity:minimal"])
+        return build_prc
+
+    def test(dirs:BuildDirs):
+        """ Perform tests """
+        os.chdir(dirs.build)
+        # Windows: change the system PATH temporarily
+        ENV["PATH"] = WindowsMake.QT_MSVC_DIR + "/bin;"
+        + dirs.python + ";"
+        + WindowsMake.PATH_INI
+        test_prc = subp.run(["ctest", "-C", "Release",
+                             "--parallel %i" % WindowsMake.NPROC, "--output-on-failure",
+                             ])
+        return test_prc
+
+    def pack(dirs:BuildDirs):
+        """ Build package via NSIS """
+        pack_prc = subp.run(["cpack", dirs.build, "-C", "Release", "-B", dirs.package])
+        return pack_prc
+
+
+def make(maker, source_dir, python_version):
+    """ Make for the given version of Python """
+    print("#--- DIAGNOSTICS ---")
+    # list all environmental variables
+    for key, val in ENV.items():
+        print(key, "=", val)
+
+    print(SEPLINE)
+
+    # set build-related directories
+    source_dir = os.path.abspath(source_dir)
+    build_dir = source_dir + "/" + BUILD_DIR
+    package_dir = "installer"
+    python_platform = ""
+    python_version = python_version.strip()
+    if python_version:
+        build_dir = build_dir + "_py" + python_version
+        package_dir = package_dir + "_py" + python_version
+        python_platform = maker.PY_PLATFORM_BASE + python_version
+
+    dirs = BuildDirs(source_dir, build_dir,
+                     build_dir + "/" + package_dir, python_platform)
+
+    print("* build directories:", dirs)
+
+    # make the CMake build directory and switch to it
+    if os.path.exists(dirs.build):
+        raise RuntimeError("Build directory '%s' already exists" % dirs.build)
+
+    os.mkdir(dirs.build)
+    os.chdir(dirs.build)
+
+    print("\n#--- CONFIGURE ---")
+    print(subp.check_output(["cmake", "--version"], text=True, encoding="utf-8"))
+    print("current path:" , os.getcwd())
+    conf_prc = maker.configure(dirs)
+    # NOTE: check_returncode() raises a `CalledProcessError` exception if the process fails
+    conf_prc.check_returncode()
+
+    print("\n#--- BUILD ---")
+    build_start = timer()
+    build_prc = maker.build(dirs)
+    build_prc.check_returncode()
+    build_elapsed = timer() - build_start
+    print(SEPLINE)
+
+    print("\n#--- TEST ---")
+    test_start = timer()
+    test_prc = maker.test(dirs)
+    test_prc.check_returncode()
+    test_elapsed = timer() - test_start
+    print(SEPLINE)
+
+    print("\n#--- PACKAGE ---")
+    print("package directory: '%s'" % package_dir)
+    pack_start = timer()
+    pack_prc = maker.pack(dirs)
+    pack_prc.check_returncode()
+    pack_elapsed = timer() - pack_start
+    print(SEPLINE)
+
+    print()
+    print("#--- Total build time = {:.3f} sec".format(build_elapsed))
+    print("#--- Total test time = {:.3f} sec".format(test_elapsed))
+    print("#--- Total packaging time = {:.3f} sec".format(pack_elapsed))
+    print()
+
+
+def make_all(source_dir="."):
+    """ Make for all versions of Python """
+    # supported platforms; see `platform.system` documentation
+    SUPPORTED_PLATFORMS = (WindowsMake.PLATFORM, MacMake_x64.PLATFORM,
+                           MacMake_arm.PLATFORM, LinuxMake.PLATFORM)
+
+    # switch to the proper make commands for the platform
+    platform_ = platform.system().lower()
+    arch_ = platform.machine().lower()
+
+    if not platform_ in SUPPORTED_PLATFORMS:
+        raise NotImplementedError(
+            "MuliPythonBuild: Platform '%s' is not in the supported platforms %s"
+            % (platform_, SUPPORTED_PLATFORMS))
+
+    if platform_ == LinuxMake.PLATFORM and arch_ == LinuxMake.ARCH:
+        maker = LinuxMake
+    elif platform_ == WindowsMake.PLATFORM and arch_ == WindowsMake.ARCH:
+        maker = WindowsMake
+    elif platform_ == MacMake_x64.PLATFORM:
+        if arch_ == MacMake_x64.ARCH:
+            maker = MacMake_x64
+        elif arch_ == MacMake_arm.ARCH:
+            maker = MacMake_arm
+        else:
+            raise NotImplementedError(
+                "MuliPythonBuild: Architecture '%s' on platform '%s' is not supported"
+                % (arch_, platform_))
+    else:
+        raise NotImplementedError(
+            "MuliPythonBuild: Platform '%s' or architecture '%s' is not supported"
+            % (platform_, arch_))
+
+    source_dir = os.path.abspath(source_dir)
+    for python_version in maker.SUPPORTED_PY_VERSIONS:
+        py_ver_str = python_version if python_version else "default"
+        print(SEPLINE)
+        print("*** Build for Python version '%s' ***" % py_ver_str)
+        make(maker, source_dir, python_version)
+        print("*** END: Build for Python version '%s' ***" % py_ver_str)
+        print(SEPLINE)
+
+
+if __name__ == "__main__":
+    make_all()
-- 
GitLab