Repository: cocotb/cocotb

Issue:
ImportError: from cocotb import simulator
Just pulled the latest from master and build is broken on windows mingw 32-bit.
All is working fine using version b08bf0ce74144886c17e6f824438890d08503a71
Could be related to this change:
https://github.com/cocotb/cocotb/commit/3a0bf9460515ae0c2269890b75ddbeaa80085cff

Here's a log when running the adder example: 
``` make SIM=modelsim
Traceback (most recent call last):
  File "C:\msys32\mingw32\bin\cocotb-config-script.py", line 11, in <module>
    load_entry_point('cocotb', 'console_scripts', 'cocotb-config')()
  File "C:/msys32/mingw32/lib/python3.8/site-packages/pkg_resources/__init__.py", line 490, in load_entry_point
    return get_distribution(dist).load_entry_point(group, name)
  File "C:/msys32/mingw32/lib/python3.8/site-packages/pkg_resources/__init__.py", line 2859, in load_entry_point
    return ep.load()
  File "C:/msys32/mingw32/lib/python3.8/site-packages/pkg_resources/__init__.py", line 2450, in load
    return self.resolve()
  File "C:/msys32/mingw32/lib/python3.8/site-packages/pkg_resources/__init__.py", line 2456, in resolve
    module = __import__(self.module_name, fromlist=['__name__'], level=0)
  File "C:/work/cocotb/cocotb/__init__.py", line 42, in <module>
    import cocotb.handle
  File "C:/work/cocotb/cocotb/handle.py", line 36, in <module>
    from cocotb import simulator
ImportError: DLL load failed while importing simulator: The specified module could not be found.
Makefile:47: /Makefile.sim: No such file or directory
make: *** No rule to make target '/Makefile.sim'.  Stop.
```
Any suggestions?


Discussion:
Can you try uninstalling and reinstalling cocotb, in case you've ended up with a mixture of installations?

@ktbarrett, any ideas?
I can reproduce.

It looks this has to be removed (for 3.8 it does not do what we expect):
https://github.com/cocotb/cocotb/blob/bd4f5c4ef7827758c99a54ae8ad41f0f035c70a4/cocotb/_os_compat.py#L17-L19

Why does this work in CI? Is our windows CI not 3.8?
I think conda is still 3.7 by default 

Maybe we can test more but would move to  azure/GitHub for this maybe?
I guess I never checked, and I guess it worked by accident, the directory is wrong...

https://github.com/cocotb/cocotb/blob/bd4f5c4ef7827758c99a54ae8ad41f0f035c70a4/cocotb/_os_compat.py#L14

That should be `extra_dll_dir = os.path.dirname(__file__)`. The simulator module is not in 'libs'. Could you confirm @themperek?
The problem is not `simulator` module but dependencies from 'libs' folder.
Is there any way to find out exactly which dll is failing to load?
On Windows, probably something like rpath can be done using manifest files. See [https://bugs.python.org/issue35688#msg335667](https://bugs.python.org/issue35688#msg335667) for a solution for a similar issue.
Perhaps the thing to do for the imminent 1.4 release is to just set `PATH` on all python versions, and come back to this later.
![cocotb_trace](https://user-images.githubusercontent.com/1379478/82056764-b0aa6400-96c2-11ea-9508-67d52c512eed.png)
For the records here is a trace. It fails to load any dependencies in the libs folder
Can you get the `AddDllDirectory` call in that trace too?
Nope, these are only the syscalls. `AddDllDirectory` probably is handled in userspace.
Which simulator are you using, @elgorwi?
It's possible we need something like https://github.com/numpy/numpy/pull/13019

Resolving patch:
diff --git a/cocotb/__init__.py b/cocotb/__init__.py
--- a/cocotb/__init__.py
+++ b/cocotb/__init__.py
@@ -39,7 +39,6 @@
 import warnings
 from typing import Dict, List, Union
 
-import cocotb._os_compat  # must appear first, before the first import of cocotb.simulator
 import cocotb.handle
 import cocotb.log
 from cocotb.scheduler import Scheduler
diff --git a/cocotb/_os_compat.py b/cocotb/_os_compat.py
deleted file mode 100644
--- a/cocotb/_os_compat.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright cocotb contributors
-# Licensed under the Revised BSD License, see LICENSE for details.
-# SPDX-License-Identifier: BSD-3-Clause
-"""
-OS-specific hacks
-"""
-import os
-import sys
-
-# This is necessary for Windows, which does not support RPATH, causing it
-# to not be able to locate the simulator module, which is imported
-# unconditionally.
-
-extra_dll_dir = os.path.join(os.path.dirname(__file__), 'libs')
-
-if sys.platform == 'win32' and os.path.isdir(extra_dll_dir):
-    # TODO[gh-1829]: investigate making os.add_dll_directory work here on Python 3.8
-    os.environ.setdefault('PATH', '')
-    os.environ['PATH'] += os.pathsep + extra_dll_dir
diff --git a/cocotb_build_libs.py b/cocotb_build_libs.py
--- a/cocotb_build_libs.py
+++ b/cocotb_build_libs.py
@@ -8,17 +8,113 @@
 import logging
 import distutils
 import subprocess
+import textwrap
 
 from setuptools import Extension
 from distutils.spawn import find_executable
 from setuptools.command.build_ext import build_ext as _build_ext
 from distutils.file_util import copy_file
+from typing import List
 
 
 logger = logging.getLogger(__name__)
 cocotb_share_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "cocotb", "share"))
 
 
+def create_sxs_assembly_manifest(name: str, filename: str, libraries: List[str]) -> str:
+    """
+    Create side-by-side (sxs) assembly manifest
+
+    It contains dependencies to other assemblies (in our case the assemblies are equal to the other libraries).
+    For more details see:
+     - https://docs.microsoft.com/en-us/windows/win32/sbscs/assembly-manifests
+     - https://docs.microsoft.com/en-us/windows/win32/sbscs/using-side-by-side-assemblies
+
+    Args:
+        name: The name of the assembly for which the manifest is generated, e.g. ``libcocotbutils``.
+        filename: The filename of the library, e.g. ``libcocotbutils.dll``.
+        libraries: A list of names of dependent manifests, e.g. ``["libgpilog"]``.
+    """
+
+    architecture = "amd64" if sys.maxsize > 2**32 else "x86"
+    dependencies = []
+
+    for lib in libraries:
+        dependencies.append(textwrap.dedent('''\
+            <dependency>
+                <dependentAssembly>
+                    <assemblyIdentity name="%s" version="1.0.0.0" type="win32" processorArchitecture="%s" />
+                </dependentAssembly>
+            </dependency>
+            ''') % (lib, architecture))
+
+    manifest_body = textwrap.dedent('''\
+        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+        <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+            <assemblyIdentity name="%s" version="1.0.0.0" type="win32" processorArchitecture="%s" />
+            <file name="%s" />
+            %s
+        </assembly>
+        ''') % (name, architecture, filename, textwrap.indent("".join(dependencies), '    ').strip())
+
+    return manifest_body
+
+
+def create_sxs_appconfig(filename):
+    """
+    Create side-by-side (sxs) application configuration file.
+
+    The application configuration specifies additional search paths for manifests.
+    For more details see: https://docs.microsoft.com/en-us/windows/win32/sbscs/application-configuration-files
+    """
+
+    config_body = textwrap.dedent('''\
+        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+        <configuration>
+            <windows>
+                <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+                    <probing privatePath="libs" />
+                </assemblyBinding>
+            </windows>
+        </configuration>
+        ''')
+
+    dirpath = os.path.dirname(filename)
+    os.makedirs(dirpath, exist_ok=True)
+
+    with open(filename + ".2.config", "w", encoding='utf-8') as f:
+        f.write(config_body)
+
+
+def create_rc_file(name, filename, libraries):
+    """
+    Creates windows resource definition script to embed the side-by-side assembly manifest into the libraries.
+
+    For more details see: https://docs.microsoft.com/en-us/windows/win32/menurc/about-resource-files
+    """
+
+    manifest = create_sxs_assembly_manifest(name, filename, libraries)
+
+    # Escape double quotes and put every line between double quotes for embedding into rc file
+    manifest = manifest.replace('"', '""')
+    manifest = '\n'.join(['"%s\\r\\n"' % x for x in manifest.splitlines()])
+
+    rc_body = textwrap.dedent('''\
+        #pragma code_page(65001) // UTF-8
+        #include <WinUser.h>
+
+        LANGUAGE 0x00, 0x00
+
+        ISOLATIONAWARE_MANIFEST_RESOURCE_ID RT_MANIFEST
+        BEGIN
+        %s
+        END
+        ''') % manifest
+
+    with open(name + ".rc", "w", encoding='utf-8') as f:
+        f.write(rc_body)
+
+
 def _get_lib_ext_name():
     """ Get name of default library file extension on given OS. """
 
@@ -36,6 +132,18 @@ def run(self):
         def_dir = os.path.join(cocotb_share_dir, "def")
         self._gen_import_libs(def_dir)
 
+        if os.name == "nt":
+            create_sxs_appconfig(self.get_ext_fullpath(os.path.join("cocotb", "simulator")))
+
+            ext_names = {os.path.split(ext.name)[-1] for ext in self.extensions}
+            for ext in self.extensions:
+                fullname = self.get_ext_fullname(ext.name)
+                filename = self.get_ext_filename(fullname)
+                name = os.path.split(fullname)[-1]
+                filename = os.path.split(filename)[-1]
+                libraries = {"lib" + lib for lib in ext.libraries}.intersection(ext_names)
+                create_rc_file(name, filename, libraries)
+
         super().run()
 
     # Needed for Windows to not assume python module (generate interface in def file)
@@ -184,11 +292,16 @@ def _get_common_lib_ext(include_dir, share_lib_dir):
     #
     #  libcocotbutils
     #
+    libcocotbutils_sources = [
+        os.path.join(share_lib_dir, "utils", "cocotb_utils.cpp")
+    ]
+    if os.name == "nt":
+        libcocotbutils_sources += ["libcocotbutils.rc"]
     libcocotbutils = Extension(
         os.path.join("cocotb", "libs", "libcocotbutils"),
         include_dirs=[include_dir],
         libraries=["gpilog"],
-        sources=[os.path.join(share_lib_dir, "utils", "cocotb_utils.cpp")],
+        sources=libcocotbutils_sources,
         extra_link_args=_extra_link_args(lib_name="libcocotbutils", rpaths=["$ORIGIN"]),
         extra_compile_args=_extra_cxx_compile_args,
     )
@@ -200,11 +313,16 @@ def _get_common_lib_ext(include_dir, share_lib_dir):
     if sys.platform == "darwin":
         python_lib_dirs = [sysconfig.get_config_var("LIBDIR")]
 
+    libgpilog_sources = [
+        os.path.join(share_lib_dir, "gpi_log", "gpi_logging.cpp")
+    ]
+    if os.name == "nt":
+        libgpilog_sources += ["libgpilog.rc"]
     libgpilog = Extension(
         os.path.join("cocotb", "libs", "libgpilog"),
         include_dirs=[include_dir],
         library_dirs=python_lib_dirs,
-        sources=[os.path.join(share_lib_dir, "gpi_log", "gpi_logging.cpp")],
+        sources=libgpilog_sources,
         extra_link_args=_extra_link_args(lib_name="libgpilog", rpaths=["$ORIGIN"]),
         extra_compile_args=_extra_cxx_compile_args,
     )
@@ -212,13 +330,18 @@ def _get_common_lib_ext(include_dir, share_lib_dir):
     #
     #  libcocotb
     #
+    libcocotb_sources = [
+        os.path.join(share_lib_dir, "embed", "gpi_embed.cpp")
+    ]
+    if os.name == "nt":
+        libcocotb_sources += ["libcocotb.rc"]
     libcocotb = Extension(
         os.path.join("cocotb", "libs", "libcocotb"),
         define_macros=[("PYTHON_SO_LIB", _get_python_lib())],
         include_dirs=[include_dir],
         libraries=[_get_python_lib_link(), "gpilog", "cocotbutils"],
         library_dirs=python_lib_dirs,
-        sources=[os.path.join(share_lib_dir, "embed", "gpi_embed.cpp")],
+        sources=libcocotb_sources,
         extra_link_args=_extra_link_args(lib_name="libcocotb", rpaths=["$ORIGIN"] + python_lib_dirs),
         extra_compile_args=_extra_cxx_compile_args,
     )
@@ -226,15 +349,18 @@ def _get_common_lib_ext(include_dir, share_lib_dir):
     #
     #  libgpi
     #
+    libgpi_sources=[
+        os.path.join(share_lib_dir, "gpi", "GpiCbHdl.cpp"),
+        os.path.join(share_lib_dir, "gpi", "GpiCommon.cpp"),
+    ]
+    if os.name == "nt":
+        libgpi_sources += ["libgpi.rc"]
     libgpi = Extension(
         os.path.join("cocotb", "libs", "libgpi"),
         define_macros=[("LIB_EXT", _get_lib_ext_name()), ("SINGLETON_HANDLES", "")],
         include_dirs=[include_dir],
         libraries=["cocotbutils", "gpilog", "cocotb", "stdc++"],
-        sources=[
-            os.path.join(share_lib_dir, "gpi", "GpiCbHdl.cpp"),
-            os.path.join(share_lib_dir, "gpi", "GpiCommon.cpp"),
-        ],
+        sources=libgpi_sources,
         extra_link_args=_extra_link_args(lib_name="libgpi", rpaths=["$ORIGIN"]),
         extra_compile_args=_extra_cxx_compile_args,
     )
@@ -242,12 +368,17 @@ def _get_common_lib_ext(include_dir, share_lib_dir):
     #
     #  simulator
     #
+    simulator_sources=[
+        os.path.join(share_lib_dir, "simulator", "simulatormodule.cpp"),
+    ]
+    if os.name == "nt":
+        simulator_sources += ["simulator.rc"]
     libsim = Extension(
         os.path.join("cocotb", "simulator"),
         include_dirs=[include_dir],
         libraries=["cocotbutils", "gpilog", "gpi"],
         library_dirs=python_lib_dirs,
-        sources=[os.path.join(share_lib_dir, "simulator", "simulatormodule.cpp")],
+        sources=simulator_sources,
         extra_compile_args=_extra_cxx_compile_args,
         extra_link_args=_extra_link_args(rpaths=["$ORIGIN/libs"]),
     )
@@ -262,16 +393,19 @@ def _get_vpi_lib_ext(
     include_dir, share_lib_dir, sim_define, extra_lib=[], extra_lib_dir=[]
 ):
     lib_name = "libcocotbvpi_" + sim_define.lower()
+    libcocotbvpi_sources = [
+        os.path.join(share_lib_dir, "vpi", "VpiImpl.cpp"),
+        os.path.join(share_lib_dir, "vpi", "VpiCbHdl.cpp"),
+    ]
+    if os.name == "nt":
+        libcocotbvpi_sources += [lib_name + ".rc"]
     libcocotbvpi = Extension(
         os.path.join("cocotb", "libs", lib_name),
         define_macros=[("VPI_CHECKING", "1")] + [(sim_define, "")],
         include_dirs=[include_dir],
         libraries=["gpi", "gpilog"] + extra_lib,
         library_dirs=extra_lib_dir,
-        sources=[
-            os.path.join(share_lib_dir, "vpi", "VpiImpl.cpp"),
-            os.path.join(share_lib_dir, "vpi", "VpiCbHdl.cpp"),
-        ],
+        sources=libcocotbvpi_sources,
         extra_link_args=_extra_link_args(lib_name=lib_name, rpaths=["$ORIGIN"]),
         extra_compile_args=_extra_cxx_compile_args,
     )
@@ -283,16 +417,19 @@ def _get_vhpi_lib_ext(
     include_dir, share_lib_dir, sim_define, extra_lib=[], extra_lib_dir=[]
 ):
     lib_name = "libcocotbvhpi_" + sim_define.lower()
+    libcocotbvhpi_sources = [
+        os.path.join(share_lib_dir, "vhpi", "VhpiImpl.cpp"),
+        os.path.join(share_lib_dir, "vhpi", "VhpiCbHdl.cpp"),
+    ]
+    if os.name == "nt":
+        libcocotbvhpi_sources += [lib_name + ".rc"]
     libcocotbvhpi = Extension(
         os.path.join("cocotb", "libs", lib_name),
         include_dirs=[include_dir],
         define_macros=[("VHPI_CHECKING", 1)] + [(sim_define, "")],
         libraries=["gpi", "gpilog", "stdc++"] + extra_lib,
         library_dirs=extra_lib_dir,
-        sources=[
-            os.path.join(share_lib_dir, "vhpi", "VhpiImpl.cpp"),
-            os.path.join(share_lib_dir, "vhpi", "VhpiCbHdl.cpp"),
-        ],
+        sources=libcocotbvhpi_sources,
         extra_link_args=_extra_link_args(lib_name=lib_name, rpaths=["$ORIGIN"]),
         extra_compile_args=_extra_cxx_compile_args,
     )
@@ -366,16 +503,19 @@ def get_ext():
         mti_path = os.path.join(modelsim_include_dir, "mti.h")
         if os.path.isfile(mti_path):
             lib_name = "libcocotbfli_modelsim"
+            fli_sources = [
+                os.path.join(share_lib_dir, "fli", "FliImpl.cpp"),
+                os.path.join(share_lib_dir, "fli", "FliCbHdl.cpp"),
+                os.path.join(share_lib_dir, "fli", "FliObjHdl.cpp"),
+            ]
+            if os.name == "nt":
+                fli_sources += [lib_name + ".rc"]
             fli_ext = Extension(
                 os.path.join("cocotb", "libs", lib_name),
                 include_dirs=[include_dir, modelsim_include_dir],
                 libraries=["gpi", "gpilog", "stdc++"] + modelsim_extra_lib,
                 library_dirs=modelsim_extra_lib_path,
-                sources=[
-                    os.path.join(share_lib_dir, "fli", "FliImpl.cpp"),
-                    os.path.join(share_lib_dir, "fli", "FliCbHdl.cpp"),
-                    os.path.join(share_lib_dir, "fli", "FliObjHdl.cpp"),
-                ],
+                sources=fli_sources,
                 extra_link_args=_extra_link_args(lib_name=lib_name, rpaths=["$ORIGIN"]),
                 extra_compile_args=_extra_cxx_compile_args,
             )


Test patch: