Repository: NTNU-IHB/PythonFMU
Pull request #21 (2020-02-10 14:01:27)

Add build on macOS
Needed for publication on conda-forge (#19) with no strict python dependency.


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 1e6c3a4..ecec2b9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -34,8 +34,8 @@ jobs:
           cmake --build . --config Release
           cd ..
 
-      - name: Compile wrapper on Linux
-        if: startsWith(runner.os, 'Linux')
+      - name: Compile wrapper on Linux / macOS
+        if: startsWith(runner.os, 'Linux') || startsWith(runner.os, 'macOS')
         run: |
           mkdir tmp-build
           cd tmp-build
@@ -70,7 +70,7 @@ jobs:
       - name: Build distribution artifacts
         run: |
           python -m pip install setuptools wheel
-          python setup.py bdist_wheel
+          python setup.py sdist bdist_wheel
 
       - name: Archive production artifacts
         uses: actions/upload-artifact@v1
@@ -102,6 +102,16 @@ jobs:
             ${{ runner.os }}-test-cache-conda-pkgs-
             ${{ runner.os }}-test-
 
+      - name: Cache conda packages for Mac OS
+        if: startsWith(runner.os, 'macOS')
+        uses: actions/cache@v1
+        with:
+          path: /usr/share/miniconda/pkgs
+          key: ${{ runner.os }}-test-cache-conda-pkgs-${{ matrix.python-version }}
+          restore-keys: |
+            ${{ runner.os }}-test-cache-conda-pkgs-
+            ${{ runner.os }}-test-
+
       - name: Cache conda packages for Windows
         if: startsWith(runner.os, 'Windows')
         uses: actions/cache@v1
diff --git a/README.md b/README.md
index f9dfe19..54cca3b 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,11 @@
-# PythonFMU (work in progress)
+# PythonFMU
 
 > A lightweight framework that enables the packaging of Python3.x code as co-simulation FMUs.
 
 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
 [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/NTNU-IHB/PythonFMU/issues)
+![CI](https://github.com/NTNU-IHB/PythonFMU/workflows/CI/badge.svg)
+![PyPI](https://img.shields.io/pypi/v/pythonfmu)
 
 [![Gitter](https://badges.gitter.im/NTNU-IHB/FMI4j.svg)](https://gitter.im/NTNU-IHB/PythonFMU?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
 
@@ -84,7 +86,10 @@ Project folders such as this will be recursively copied into the FMU. Multiple p
 
 PythonFMU does not bundle Python, which makes it a tool coupling solution. This means that you can not expect the generated FMU to work on a different system (The system would need a compatible Python version and libraries). But to ease its usage the wrapper is compile using
 the limited Python API. So the provided binary libraries for Linux and Windows 64-bits should
-be compatible of any Python 3 environment.
+be compatible with any Python 3 environment. If you need to compile the wrapper for on a specific configuration,
+you will need cmake and a C++ compiler. The commands for building the wrapper on Linux and on Windows can be seen in 
+the [GitHub workflow](./.github/workflows/main.yml).
+
 PythonFMU does not automatically resolve 3rd party dependencies either. If your code includes e.g. `numpy`, the target system also needs to have `numpy` installed.
 
 ***
diff --git a/pythonfmu/_version.py b/pythonfmu/_version.py
index 49f34f4..493f741 100644
--- a/pythonfmu/_version.py
+++ b/pythonfmu/_version.py
@@ -1,1 +1,1 @@
-__version__ = "0.2.0"
\ No newline at end of file
+__version__ = "0.3.0"
diff --git a/pythonfmu/builder.py b/pythonfmu/builder.py
index 7cc7b5d..90a6b49 100644
--- a/pythonfmu/builder.py
+++ b/pythonfmu/builder.py
@@ -20,9 +20,13 @@ HERE = Path(__file__).parent
 
 logger = logging.getLogger(__name__)
 
-lib_prefix = "lib" if platform.system() == "Linux" else ""
 
-lib_extension = ({"Darwin": "so", "Linux": "so", "Windows": "dll"}).get(platform.system(), None)
+def get_platform() -> str:
+    """Get FMU binary platform folder name."""
+    system = platform.system()
+    is_64bits = sys.maxsize > 2 ** 32
+    platforms = {"Windows": "win", "Linux": "linux", "Darwin": "darwin"}
+    return platforms.get(system, "unknown") + "64" if is_64bits else "32"
 
 
 class ModelDescriptionFetcher:
@@ -155,7 +159,7 @@ class FmuBuilder:
                 binaries = Path("binaries")
                 src_binaries = HERE / "resources" / "binaries"
                 for f in itertools.chain(
-                    src_binaries.rglob("*.dll"), src_binaries.rglob("*.so")
+                    src_binaries.rglob("*.dll"), src_binaries.rglob("*.so"), src_binaries.rglob("*.dylib")
                 ):
                     relative_f = f.relative_to(src_binaries)
                     arcname = (
@@ -179,6 +183,13 @@ class FmuBuilder:
                     "modelDescription.xml", xml_str.toprettyxml(encoding="UTF-8")
                 )
 
+    @staticmethod
+    def has_binary() -> bool:
+        """Does the binary for this platform exits?"""
+        binary_folder = get_platform()
+        src_binaries = HERE / "resources" / "binaries" / binary_folder
+        return src_binaries.exists() and len(list(src_binaries.iterdir())) == 1
+
 
 def main():
     parser = argparse.ArgumentParser(
diff --git a/pythonfmu/pythonfmu-export/cpp/PyObjectWrapper.cpp b/pythonfmu/pythonfmu-export/cpp/PyObjectWrapper.cpp
index 84b0ffa..a2bc162 100644
--- a/pythonfmu/pythonfmu-export/cpp/PyObjectWrapper.cpp
+++ b/pythonfmu/pythonfmu-export/cpp/PyObjectWrapper.cpp
@@ -42,7 +42,7 @@ PyObjectWrapper::PyObjectWrapper(const std::string& instanceName, const std::str
     int success = PyList_Insert(sys_path, 0, PyUnicode_FromString(resources.c_str()));
     Py_DECREF(sys_path);
     if (success != 0) {
-        handle_py_exception("[ctor] PyList_Append");
+        handle_py_exception("[ctor] PyList_Insert");
     }
 
     auto moduleName = getline(resources + "/slavemodule.txt");
diff --git a/pythonfmu/pythonfmu-export/headers/cppfmu/cppfmu_common.hpp b/pythonfmu/pythonfmu-export/headers/cppfmu/cppfmu_common.hpp
index 874718f..18dd404 100644
--- a/pythonfmu/pythonfmu-export/headers/cppfmu/cppfmu_common.hpp
+++ b/pythonfmu/pythonfmu-export/headers/cppfmu/cppfmu_common.hpp
@@ -214,7 +214,7 @@ public:
 
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wmissing-field-initializers"
-    Allocator() : m_memory{fmiCallbackFunctions{}} { }
+    Allocator() : m_memory{FMICallbackFunctions{}} { }
 #pragma GCC diagnostic pop
 #endif
 
diff --git a/setup.cfg b/setup.cfg
index 25452b6..73c5c61 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -34,6 +34,7 @@ zip_safe = False
 [options.package_data]
 pythonfmu = 
 	resources/*.dll
+	resources/*.dylib
 	resources/*.so
 
 [options.entry_points]


Test patch:
diff --git a/pythonfmu/tests/test_builder.py b/pythonfmu/tests/test_builder.py
index bfd9678..5d2384d 100644
--- a/pythonfmu/tests/test_builder.py
+++ b/pythonfmu/tests/test_builder.py
@@ -8,7 +8,7 @@ from pathlib import Path
 import pytest
 
 import pythonfmu
-from pythonfmu.builder import FmuBuilder
+from pythonfmu.builder import FmuBuilder, get_platform
 
 DEMO = "pythonslave.py"
 PROJECT_TEST_CASES = [
@@ -19,14 +19,7 @@ PROJECT_TEST_CASES = [
 ]
 
 
-def get_platform():
-    system = platform.system()
-    is_64bits = sys.maxsize > 2 ** 32
-    platforms = {"Windows": "win", "Linux": "linux", "Darwin": "darwin"}
-    return platforms[system] + "64" if is_64bits else "32"
-
-
-lib_extension = ({"Darwin": "so", "Linux": "so", "Windows": "dll"}).get(
+lib_extension = ({"Darwin": "dylib", "Linux": "so", "Windows": "dll"}).get(
     platform.system(), None
 )
 
@@ -47,10 +40,15 @@ def test_zip_content(tmp_path):
         assert "/".join(("resources", DEMO)) in names
         module_file = "/".join(("resources", "slavemodule.txt"))
         assert module_file in names
-        assert (
-            "/".join(("binaries", get_platform(), f"PythonSlave.{lib_extension}"))
-            in names
-        )
+
+        nfiles = 15 
+        if FmuBuilder.has_binary():
+            assert (
+                "/".join(("binaries", get_platform(), f"PythonSlave.{lib_extension}"))
+                in names
+            )
+        else:
+            nfiles = 14
 
         # Check sources
         src_folder = Path(pythonfmu.__path__[0]) / "pythonfmu-export"
@@ -60,7 +58,8 @@ def test_zip_content(tmp_path):
             src_folder.rglob("CMakeLists.txt"),
         ):
             assert "/".join(("sources", f.relative_to(src_folder).as_posix())) in names
-        assert len(names) >= 15  # Library + python script + XML + module name + sources
+
+        assert len(names) >= nfiles  # Library + python script + XML + module name + sources
 
         with files.open(module_file) as myfile:
             assert myfile.read() == b"pythonslave"
diff --git a/pythonfmu/tests/test_integration.py b/pythonfmu/tests/test_integration.py
index 93c52c3..f3f1bd6 100644
--- a/pythonfmu/tests/test_integration.py
+++ b/pythonfmu/tests/test_integration.py
@@ -5,6 +5,7 @@ import pytest
 
 from pythonfmu.builder import FmuBuilder
 
+pytestmark = pytest.mark.skipif(not FmuBuilder.has_binary(), reason="No binary available for the current platform.")
 pyfmi = pytest.importorskip(
     "pyfmi", reason="pyfmi is required for testing the produced FMU"
 )
@@ -15,7 +16,6 @@ DEMO = "pythonslave.py"
 
 @pytest.mark.integration
 def test_integration_demo(tmp_path):
-
     script_file = Path(__file__).parent / DEMO
 
     FmuBuilder.build_FMU(script_file, dest=tmp_path, needsExecutionTool="false")