Repository: zooba/pymsbuild (language: python)

Issue:
Add EntryPoint project type
Allow defining/generating a native entry point executable, suitable for use with an embeddable Python distro.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index d20e65c..706c7e0 100644
--- a/README.md
+++ b/README.md
@@ -875,3 +875,55 @@ When building native components on POSIX, a `python3-config` script is
 needed to determine compilation options. By default, only the location
 adjacent to the running interpreter is checked. This may be overridden
 by setting the `PYTHON_CONFIG` variable to the preferred command.
+
+## Custom entry point
+
+**Experimental.**
+
+To generate an executable that will launch your application, include the
+`pymsbuild.entrypoint` module and use an `Entrypoint` definition.
+
+```python
+from pymsbuild import *
+from pymsbuild.entrypoint import *
+
+PACKAGE = Package(
+    "demo",
+    Entrypoint(
+        "run",  # generate run.exe
+        "app",  # import app
+        "main", # app.main()
+        Icon("app.ico"),
+
+        # Search paths for the entry point to use
+        SearchPath("."),
+        SearchPath("stdlib.zip"),
+
+        # Include a copy of Python (default: True)
+        IncludePythonRuntime=True,
+        # Use the embeddable distro (default: True)
+        PythonEmbeddable=True,
+        # Also include python.exe (default: False)
+        PythonExecutables=False,
+        # Rename pythonXY.zip to stdlib.zip (default: True)
+        PythonRuntimeRenameStdlibZip=True,
+    ),
+    Package(
+        "app",
+        PyFile("app/__init__.py"),
+    ),
+)
+```
+
+Building this definition will create a `demo` directory containing
+`run.exe`, `app.py` and a copy of the Python embeddable runtime, making
+it an entirely standalone and redistributable application.
+
+Set `IncludePythonRuntime` to `False` to omit the runtime. The generated
+executable assumes that it will be able to load the version of Python
+used to build at runtime, so you will need to include it some other way.
+
+Use `SearchPath` items to specify directories to search for Python
+modules at runtime. These are the only directories that will be
+searched, as Python will be loaded in isolated mode. They are relative
+to the entrypoint and will be resolved when executing.
diff --git a/pymsbuild/__init__.py b/pymsbuild/__init__.py
index 6f531fa..3275106 100644
--- a/pymsbuild/__init__.py
+++ b/pymsbuild/__init__.py
@@ -1,7 +1,7 @@
 """The pymsbuild build backend.
 """
 
-__version__ = "%VERSION%"
+__version__ = "0.0.1"
 try:
     NEXT_INCOMPATIBLE_VERSION = "{}.0".format(int(__version__.partition(".")[0]) + 1)
     PYMSBUILD_REQUIRES_SPEC = f"pymsbuild>={__version__},<{NEXT_INCOMPATIBLE_VERSION}"
diff --git a/pymsbuild/_build.py b/pymsbuild/_build.py
index 5f448ab..c21735c 100644
--- a/pymsbuild/_build.py
+++ b/pymsbuild/_build.py
@@ -81,8 +81,13 @@ def __init__(self, output_dir=None):
         self.target = None
         self.msbuild_exe = None
         self.output_dir = Path(output_dir) if output_dir else None
-        self.build_dir = None
-        self.temp_dir = None
+        if os.getenv("PYMSBUILD_TEMP_DIR"):
+            root_dir = Path(os.getenv("PYMSBUILD_TEMP_DIR"))
+            self.build_dir = root_dir / "bin"
+            self.temp_dir = root_dir / "temp"
+        else:
+            self.build_dir = None
+            self.temp_dir = None
         self._perform_layout = None
         self.layout_dir = None
         self.layout_extra_files = []
@@ -111,7 +116,7 @@ def finalize_metadata(self, getenv=os.getenv, sdist=False, in_place=False):
         self.build_dir = self.source_dir / (self.build_dir or "build/bin")
         if self._perform_layout is None:
             self._perform_layout = bool(self.layout_dir)
-        self.layout_dir = self.source_dir / (self.layout_dir or "build/layout")
+        self.layout_dir = self.source_dir / (self.layout_dir or (self.build_dir / "layout"))
         self.temp_dir = self.source_dir / (self.temp_dir or "build/temp")
         self.pkginfo = self.source_dir / (self.pkginfo or "PKG-INFO")
         self.metadata_dir = self.temp_dir / (self.metadata_dir or "metadata")
@@ -266,14 +271,6 @@ def generate(self):
             self.log("Generating", self.pkginfo)
             _generate.generate_distinfo(self.metadata, self.temp_dir, self.source_dir)
 
-        ext = self.ext_suffix
-        self.log("Setting missing TargetExt to", ext)
-
-        from . import _types as T
-        for p in self.package:
-            if isinstance(p, T.PydFile):
-                p.options["TargetExt"] = p.options.get("TargetExt") or ext
-
         self.log("Generating projects")
         self.project = Path(_generate.generate(
             self.package,
@@ -299,12 +296,20 @@ def build(self, **properties):
                 self.write("WARNING:", self.platform, "is not a known platform. Projects may not compile")
                 properties["Platform"] = self.platform
         properties.setdefault("HostPython", sys.executable)
+        if sys.base_prefix != sys.prefix:
+            base_host = getattr(sys, "_base_executable", None)
+            if not base_host or base_host == sys.executable:
+                base_host = Path(sys.base_prefix) / Path(sys.executable).relative_to(sys.prefix)
+            properties.setdefault("BaseHostPython", base_host)
+        else:
+            properties.setdefault("BaseHostPython", sys.executable)
         properties.setdefault("PyMsbuildTargets", self.targets)
         properties.setdefault("_ProjectBuildTarget", self.target)
         properties.setdefault("SourceRootDir", self.source_dir)
         properties.setdefault("OutDir", self.build_dir)
         properties.setdefault("IntDir", self.temp_dir)
         properties.setdefault("LayoutDir", self.layout_dir)
+        properties.setdefault("DefaultExtSuffix", self.ext_suffix)
         properties.setdefault("PythonConfig", self.python_config)
         properties.setdefault("PythonIncludes", self.python_includes)
         properties.setdefault("PythonLibs", self.python_libs)
diff --git a/pymsbuild/_generate.py b/pymsbuild/_generate.py
index c70be8f..d76ee77 100644
--- a/pymsbuild/_generate.py
+++ b/pymsbuild/_generate.py
@@ -2,7 +2,7 @@
 import sys
 
 from pathlib import PurePath, Path
-from ._types import Package, PydFile, File, LiteralXML, Property, ItemDefinition, ConditionalValue
+from ._types import Package, CProject, File, LiteralXML, Property, ItemDefinition, ConditionalValue
 from ._writer import ProjectFileWriter
 
 from importlib.machinery import EXTENSION_SUFFIXES
@@ -189,7 +189,7 @@ def _write_members(f, source_dir, members):
                 p.write_member(f, g)
 
 
-def _generate_pyd(project, build_dir, root_dir):
+def _generate_c_project(project, build_dir, root_dir):
     build_dir = Path(build_dir)
     proj = build_dir / "{}.proj".format(project.name)
     root_dir = Path(root_dir)
@@ -198,42 +198,31 @@ def _generate_pyd(project, build_dir, root_dir):
     if project.project_file:
         return Path(project.project_file)
 
-    tpath = project.options.get("TargetName", project.name) + (project.options.get("TargetExt") or DEFAULT_PYD_SUFFIX)
-    tname, tdot, text = tpath.rpartition(".")
+    tname = project.options.get("TargetName", project.name)
     with ProjectFileWriter(proj, tname, vc_platforms=True, root_namespace=project.name) as f:
         with f.group("PropertyGroup", Label="Globals"):
             f.add_property("SourceDir", ConditionalValue(source_dir, if_empty=True))
             f.add_property("SourceRootDir", ConditionalValue(root_dir, if_empty=True))
-            f.add_property("__TargetExt", tdot + text)
         _write_project_references(f, project, build_dir, source_dir)
         _write_members(f, source_dir, _all_members(project, recurse_if=lambda m: m is project))
         for n, p in _all_members(project, recurse_if=lambda m: m is project, return_if=lambda m: isinstance(m, Package)):
             _write_members(
                 f,
                 source_dir,
-                _all_members(p, recurse_if=lambda m: not isinstance(m, PydFile), prefix=f"{project.name}/")
+                _all_members(p, recurse_if=lambda m: not isinstance(m, CProject), prefix=f"{project.name}/")
             )
 
     return proj
 
 
-def _generate_pyd_reference_metadata(relname, project, source_dir):
-    output = project.options.get("TargetName", relname.stem) + project.options.get("TargetExt", DEFAULT_PYD_SUFFIX)
-    tname, tdot, text = output.rpartition(".")
-    tdir = relname.parent
-    if str(tdir) == ".":
-        tdir = ""
+def _generate_reference_metadata(relname, project, source_dir):
+    tname = project.options.get("TargetName", relname.stem)
+    tdir = str(relname.parent)
     return {
-        **dict(
-            TargetDir=tdir,
-            IntDir="$(IntDir)" + tname,
-        ),
+        "TargetDir": "" if tdir == "." else tdir,
+        "IntDir": "$(IntDir)" + tname,
         **project.options,
-        **dict(
-            TargetName=tname,
-            TargetExt=tdot + text,
-            SourceDir=source_dir / project.source,
-        ),
+        "SourceDir": source_dir / project.source,
     }
 
 
@@ -241,11 +230,11 @@ def _write_project_references(f, project, build_dir, source_dir):
     with f.group("ItemGroup", Label="ProjectReferences"):
         for n, p in _all_members(
             project,
-            return_if=lambda m: m is not project and isinstance(m, PydFile),
-            make_prefix=lambda prefix, item: "{}{}/".format(prefix, item.name) if not isinstance(item, PydFile) else prefix,
+            return_if=lambda m: m is not project and isinstance(m, CProject),
+            make_prefix=lambda prefix, item: "{}{}/".format(prefix, item.name) if not isinstance(item, CProject) else prefix,
         ):
             fn = PurePath(n)
-            pdir = _generate_pyd(p, build_dir, source_dir)
+            pdir = _generate_c_project(p, build_dir, source_dir)
             try:
                 pdir = pdir.relative_to(Path(f.filename).parent)
             except ValueError:
@@ -254,7 +243,7 @@ def _write_project_references(f, project, build_dir, source_dir):
                 "Project",
                 pdir,
                 Name=n,
-                **_generate_pyd_reference_metadata(fn, p, source_dir),
+                **_generate_reference_metadata(fn, p, source_dir),
             )
 
 
@@ -268,8 +257,8 @@ def generate(project, build_dir, source_dir, config_file=None):
     if project.project_file:
         return Path(project.project_file)
 
-    if isinstance(project, PydFile):
-        return _generate_pyd(project, build_dir, root_dir)
+    if isinstance(project, CProject):
+        return _generate_c_project(project, build_dir, root_dir)
 
     with ProjectFileWriter(proj, project.name) as f:
         with f.group("PropertyGroup"):
@@ -286,7 +275,7 @@ def generate(project, build_dir, source_dir, config_file=None):
             f.add_item("Sdist", root_dir / "pyproject.toml", RelativeSource="pyproject.toml")
         _write_members(f, source_dir, _all_members(
             project,
-            recurse_if=lambda m: not isinstance(m, PydFile),
+            recurse_if=lambda m: not isinstance(m, CProject),
         ))
         f.add_import(f"$(PyMsbuildTargets){SEP}common.targets")
         f.add_import(f"$(PyMsbuildTargets){SEP}package.targets")
diff --git a/pymsbuild/_types.py b/pymsbuild/_types.py
index 1fba73b..ea9d7d5 100644
--- a/pymsbuild/_types.py
+++ b/pymsbuild/_types.py
@@ -4,7 +4,9 @@
 __all__ = [
     "Package",
     "Project",
+    "CProject",
     "PydFile",
+    "VersionInfo",
     "LiteralXML",
     "ConditionalValue",
     "ExactNameMatchMixin",
@@ -222,12 +224,18 @@ class Package(_Project):
 """
 
 
-class PydFile(_Project):
-    r"""Represents a .pyd module.
+class CProject(_Project):
+    r"""Represents a native binary.
 
-Each PydFile represents a single .pyd file in the final distribution.
-It must be included in a Package, and the built module will be
-included in that package's directory.
+Use PydFile to create a native Python extension module, unless
+you are avoiding automatic filename generation.
+
+Each CProject represents a single compiled file in the final
+distribution. It must be included in a Package, and the built
+file will be included in that package's directory.
+
+By default it will produce a dynamic library. Specify
+'ConfigurationType' as 'Application' or 'StaticLibrary' to change.
 
 Its members should be source files or projects. Directly referenced
 files will be included in sdists, but transitive references will not.
@@ -242,39 +250,45 @@ class PydFile(_Project):
 """
     options = {
         "ConfigurationType": "DynamicLibrary",
-        "TargetExt": None,
     }
 
     def __init__(self, name, *members, **options):
         super().__init__(name, *members, **options)
         self.members = [
             self.GlobalProperties(self),
-            self.DefaultToolsetImports(),
+            self.DefaultToolsetProps(),
             self.ConfigurationProperties(self),
-            self.ToolsetImports(),
+            self.ToolsetProps(),
+            self.TargetExtProperty(self),
+            self.ToolsetPydProps(),
             *self.members,
             self.ToolsetTargets(),
         ]
 
 
-    class DefaultToolsetImports(ImportGroup):
-        name = "$PydFile.DefaultCppImports"
+    class DefaultToolsetProps(ImportGroup):
+        name = "$CProject.DefaultCppProps"
         imports = [
             "$(PyMsbuildTargets)/common.props",
             "$(PyMsbuildTargets)/cpp-default-$(Platform).props",
         ]
 
 
-    class ToolsetImports(ImportGroup):
-        name = "$PydFile.CppImports"
+    class ToolsetProps(ImportGroup):
+        name = "$CProject.CppImports"
         imports = [
             "$(PyMsbuildTargets)/cpp-$(Platform).props",
+        ]
+
+    class ToolsetPydProps(ImportGroup):
+        name = "$CProject.PydProps"
+        imports = [
             "$(PyMsbuildTargets)/pyd.props",
         ]
 
 
     class ToolsetTargets(ImportGroup):
-        name = "$PydFile.CppTargets"
+        name = "$CProject.CppTargets"
         imports = [
             "$(PyMsbuildTargets)/common.targets",
             "$(PyMsbuildTargets)/cpp-$(Platform).targets",
@@ -291,7 +305,7 @@ class GlobalProperties(ExactNameMatchMixin):
 modify its contents.
 """
         members = ()
-        name = "$PydFile.GlobalProperties"
+        name = "$CProject.GlobalProperties"
         defer = {"ConfigurationType", "TargetExt"}
 
         def __init__(self, project):
@@ -306,7 +320,7 @@ def write_member(self, f, g):
 
     class ConfigurationProperties(ExactNameMatchMixin):
         members = ()
-        name = "$PydFile.ConfigurationProperties"
+        name = "$CProject.ConfigurationProperties"
 
         def __init__(self, project):
             self.project = project
@@ -319,6 +333,53 @@ def write_member(self, f, g):
             f.add_property("CharacterSet", "Unicode")
 
 
+    class TargetExtProperty(ExactNameMatchMixin):
+        members = ()
+        name = "$CProject.TargetExtProperty"
+
+        def __init__(self, project):
+            self.project = project
+
+        def write_member(self, f, g):
+            try:
+                target_ext = self.project.options["TargetExt"]
+            except LookupError:
+                pass
+            else:
+                g.switch_to("PropertyGroup")
+                f.add_property("TargetExt", target_ext)
+
+    # BACK-COMPAT: Expose under old name as well
+    DefaultToolsetImports = DefaultToolsetProps
+    ToolsetImports = ToolsetProps
+
+
+class PydFile(CProject):
+    r"""Represents a .pyd module.
+
+Each PydFile represents a single .pyd file in the final distribution.
+It must be included in a Package, and the built module will be
+included in that package's directory.
+
+Its members should be source files or projects. Directly referenced
+files will be included in sdists, but transitive references will not.
+
+Specify `project_file` to override all generation with a fixed
+MSBuild project file. This also overrides additional options and
+sdist generation.
+
+Specify `source` to find sources in a subdirectory.
+
+Other options will be added to the project as properties.
+"""
+    options = {
+        **CProject.options,
+        "ConfigurationType": "ExtensionModule",
+        "LinkCompiled": "true",
+        "OutputType": "library",
+    }
+
+
 class VersionInfo:
     r"""Represents version info to compile into a .pyd module.
 
@@ -383,11 +444,11 @@ def _read_version(version):
                 break
             else:
                 vv.append(v)
-        return ", ".join((vv + ("0", "0", "0", "0"))[:4])
+        return ", ".join((*vv, "0", "0", "0", "0")[:4])
 
     def from_metadata(self, metadata):
         ver = metadata["Version"]
-        self.update(False, dict(
+        self.update(False, **dict(
             FILEVERSION=self._read_version(ver),
             PRODUCTVERSION=self._read_version(ver),
             CompanyName=metadata.get("Author"),
@@ -682,7 +743,6 @@ class Midl(File):
         "Subtype": "Code",
     }
 
-
 class Manifest(File):
     r"""Add an application manifest file.
 
@@ -693,5 +753,3 @@ class Manifest(File):
 for more information.
 """
     _ITEMNAME = "Manifest"
-
-
diff --git a/pymsbuild/entrypoint.py b/pymsbuild/entrypoint.py
new file mode 100644
index 0000000..278b131
--- /dev/null
+++ b/pymsbuild/entrypoint.py
@@ -0,0 +1,77 @@
+import os
+
+from pymsbuild._types import *
+
+__all__ = ['Entrypoint', 'Icon', 'SearchPath', 'DefaultSearchPath']
+
+class Entrypoint(CProject):
+    r"""Represents an executable that loads Python and calls your function.
+"""
+
+    class EntrypointProps:
+        members = ()
+        name = "$Entrypoint.EntrypointProps"
+        def write_member(self, f, g):
+            g.switch_to(None)
+            f.add_import(f"$(PyMsbuildTargets){os.path.sep}entrypoint.props")
+
+    class EntrypointImports(ImportGroup):
+        name = "$Entrypoint.Imports"
+        imports = [f"$(PyMsbuildTargets){os.path.sep}entrypoint.targets"]
+
+    def __init__(self, name, module, function, *members, **kwargs):
+        kwargs.setdefault("ConfigurationType", "Application")
+        kwargs.setdefault("EntrypointModule", module)
+        kwargs.setdefault("EntrypointFunction", function)
+        super().__init__(name, *members, **kwargs)
+        self.insert(
+            CProject.GlobalProperties.name,
+            self.EntrypointProps(),
+            offset=1,
+        )
+        self.members.append(self.EntrypointImports())
+
+
+class Icon(File):
+    r"""Represents an icon resource.
+"""
+    _ITEMNAME = "EntrypointIcon"
+    options = {
+        **File.options,
+        "IncludeInLayout": False,
+        "IncludeInWheel": False,
+    }
+
+
+class SearchPath:
+    r"""Represents a search path in an entrypoint executable.
+
+Paths are relative to the location of the executable."""
+    _ITEMNAME = "EntrypointPythonPath"
+    members = ()
+    options = {}
+
+    def __init__(self, name, **options):
+        self.name = name
+        self.options = {
+            **self.options,
+            **options,
+        }
+
+    def write_member(self, project, group):
+        group.switch_to("ItemGroup")
+        project.add_item(self._ITEMNAME, self.name, **self.options)
+
+
+class DefaultSearchPath(SearchPath):
+    r"""Represents a default Python search path.
+
+This will vary depending on how Python is being included or referenced
+from the built executable. For portability/relocatability, you are
+advised to avoid this option. However, it's fine for when building to
+run on the same machine.
+"""
+    _ITEMNAME = "DefaultEntrypointPythonPath"
+
+    def __init__(self, name="$DefaultPythonPath", **options):
+        super().__init__(name, **options)
diff --git a/pymsbuild/targets/common.props b/pymsbuild/targets/common.props
index c96c2cd..3139f60 100644
--- a/pymsbuild/targets/common.props
+++ b/pymsbuild/targets/common.props
@@ -5,6 +5,8 @@
     <_Sep Condition="$([msbuild]::IsOSUnixLike())">/</_Sep>
     <!-- Use EscapeSep after an EnsureTrailingSlash to double the backslash when needed -->
     <_EscapeSep Condition="$([msbuild]::EnsureTrailingSlash(`a`)) == 'a\'">\</_EscapeSep>
+    <_PathSep>;</_PathSep>
+    <_PathSep Condition="$([msbuild]::IsOSUnixLike())">:</_PathSep>
 
     <LayoutDir Condition="$(LayoutDir) == ''">layout$(_Sep)</LayoutDir>
     <OutDir Condition="$(OutDir) == ''">bin$(_Sep)</OutDir>
diff --git a/pymsbuild/targets/common.targets b/pymsbuild/targets/common.targets
index f3555fc..a560e31 100644
--- a/pymsbuild/targets/common.targets
+++ b/pymsbuild/targets/common.targets
@@ -1,36 +1,45 @@
 <Project>
-  <Target Name="_GetPackageFilesFromProjects" Condition="@(Project) != ''">
+  <Target Name="_GetPackageFilesFromEachProject" Inputs="@(Project)" Outputs="%(Project.Identity).__donotbuild">
+    <Message Text="Getting package files from %(Project.Name) %(Project.TargetDir)" Importance="$(_Low)" />
     <ItemGroup>
       <_PackageFiles Remove="@(_PackageFiles)" />
       <_LayoutFiles Remove="@(_LayoutFiles)" />
     </ItemGroup>
-    <Message Text="Getting package files from %(Project.Name)" />
-    <MSBuild Projects="%(Project.Identity)"
-             Properties="%(Project.Properties)"
+    <MSBuild Projects="@(Project)"
+             Properties="%(Properties)"
              Targets="GetPackageFiles"
              RebaseOutputs="True">
       <Output TaskParameter="TargetOutputs" ItemName="_PackageFiles" />
     </MSBuild>
-    <MSBuild Projects="%(Project.Identity)"
-             Properties="%(Project.Properties)"
+    <MSBuild Projects="@(Project)"
+             Properties="%(Properties)"
              Targets="GetLayoutFiles"
              RebaseOutputs="True">
       <Output TaskParameter="TargetOutputs" ItemName="_LayoutFiles" />
     </MSBuild>
     <ItemGroup>
-      <PyMSBuild_PackageFiles Include="@(_PackageFiles)" />
+      <PyMSBuild_PackageFiles Include="@(_PackageFiles)">
+        <TargetDir Condition="%(TargetDir) == ''">%(Project.TargetDir)</TargetDir>
+      </PyMSBuild_PackageFiles>
+      <PyMSBuild_LayoutFiles Include="@(_LayoutFiles)">
+        <TargetDir Condition="%(TargetDir) == ''">%(Project.TargetDir)</TargetDir>
+      </PyMSBuild_LayoutFiles>
       <_PackageFiles Remove="@(_PackageFiles)" />
-      <PyMSBuild_LayoutFiles Include="@(_LayoutFiles)" />
       <_LayoutFiles Remove="@(_LayoutFiles)" />
     </ItemGroup>
   </Target>
 
-  <Target Name="_GetSdistFilesFromProjects" Condition="@(Project) != ''">
+  <Target Name="_GetPackageFilesFromProjects" DependsOnTargets="_GetPackageFilesFromEachProject">
+  </Target>
+
+  <Target Name="_GetSdistFilesFromEachProject" Inputs="@(Project)" Outputs="%(Project.Identity).__donotbuild">
+    <Message Text="Getting sdist files from %(Project.FullName)" />
     <ItemGroup>
       <_PackageFiles Remove="@(_PackageFiles)" />
     </ItemGroup>
-    <Message Text="Getting sdist files from %(Project.FullName)" />
-    <MSBuild Projects="%(Project.Identity)" Properties="%(Project.Properties)" Targets="GetSdistFiles"
+    <MSBuild Projects="@(Project)"
+             Properties="%(Project.Properties)"
+             Targets="GetSdistFiles"
              RebaseOutputs="True">
       <Output TaskParameter="TargetOutputs" ItemName="_PackageFiles" />
     </MSBuild>
@@ -44,6 +53,9 @@
     </ItemGroup>
   </Target>
 
+  <Target Name="_GetSdistFilesFromProjects" DependsOnTargets="_GetSdistFilesFromEachProject">
+  </Target>
+
   <Target Name="GetPackageFiles" DependsOnTargets="$(GetPackageFilesTargets)" Returns="@(PyMSBuild_PackageFiles)">
     <ItemGroup>
       <_WithMetadata Remove="@(_WithMetadata)" />
diff --git a/pymsbuild/targets/cpp-ARM.props b/pymsbuild/targets/cpp-ARM.props
index d097527..5a9addf 100644
--- a/pymsbuild/targets/cpp-ARM.props
+++ b/pymsbuild/targets/cpp-ARM.props
@@ -1,3 +1,7 @@
 <Project>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" Condition="$(VCTargetsPath) != ''" />
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.WinDLL.props" Condition="$(ConfigurationType) == 'ExtensionModule' and $(VCTargetsPath) != ''" />
+  <PropertyGroup>
+    <TargetExt Condition="$(ConfigurationType) == 'ExtensionModule'">$(DefaultExtSuffix)</TargetExt>
+  </PropertyGroup>
 </Project>
diff --git a/pymsbuild/targets/cpp-ARM.targets b/pymsbuild/targets/cpp-ARM.targets
index ffbdc14..69e1919 100644
--- a/pymsbuild/targets/cpp-ARM.targets
+++ b/pymsbuild/targets/cpp-ARM.targets
@@ -1,10 +1,11 @@
 <Project>
   <Target Name="_GetPdbPackageFiles">
     <ItemGroup>
-      <AllSourceFiles Include="$(OutDir)$(TargetName).pdb" Condition="$(Configuration) == 'Debug'">
+      <_PdbFile Include="$([System.IO.Path]::ChangeExtension($(TargetPath), '.pdb'))" Condition="$(Configuration) == 'Debug'" />
+      <AllSourceFiles Include="@(_PdbFile)">
         <TargetDir></TargetDir>
-        <TargetName>$(TargetName)</TargetName>
-        <TargetExt>.pdb</TargetExt>
+        <TargetName>%(Filename)</TargetName>
+        <TargetExt>%(Extension)</TargetExt>
         <IncludeInWheel>true</IncludeInWheel>
         <IncludeInSdist>false</IncludeInSdist>
       </AllSourceFiles>
diff --git a/pymsbuild/targets/cpp-ARM64.props b/pymsbuild/targets/cpp-ARM64.props
index d097527..5a9addf 100644
--- a/pymsbuild/targets/cpp-ARM64.props
+++ b/pymsbuild/targets/cpp-ARM64.props
@@ -1,3 +1,7 @@
 <Project>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" Condition="$(VCTargetsPath) != ''" />
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.WinDLL.props" Condition="$(ConfigurationType) == 'ExtensionModule' and $(VCTargetsPath) != ''" />
+  <PropertyGroup>
+    <TargetExt Condition="$(ConfigurationType) == 'ExtensionModule'">$(DefaultExtSuffix)</TargetExt>
+  </PropertyGroup>
 </Project>
diff --git a/pymsbuild/targets/cpp-ARM64.targets b/pymsbuild/targets/cpp-ARM64.targets
index ffbdc14..69e1919 100644
--- a/pymsbuild/targets/cpp-ARM64.targets
+++ b/pymsbuild/targets/cpp-ARM64.targets
@@ -1,10 +1,11 @@
 <Project>
   <Target Name="_GetPdbPackageFiles">
     <ItemGroup>
-      <AllSourceFiles Include="$(OutDir)$(TargetName).pdb" Condition="$(Configuration) == 'Debug'">
+      <_PdbFile Include="$([System.IO.Path]::ChangeExtension($(TargetPath), '.pdb'))" Condition="$(Configuration) == 'Debug'" />
+      <AllSourceFiles Include="@(_PdbFile)">
         <TargetDir></TargetDir>
-        <TargetName>$(TargetName)</TargetName>
-        <TargetExt>.pdb</TargetExt>
+        <TargetName>%(Filename)</TargetName>
+        <TargetExt>%(Extension)</TargetExt>
         <IncludeInWheel>true</IncludeInWheel>
         <IncludeInSdist>false</IncludeInSdist>
       </AllSourceFiles>
diff --git a/pymsbuild/targets/cpp-POSIX_x64-GCC.props b/pymsbuild/targets/cpp-POSIX_x64-GCC.props
index 9661f6d..0373e26 100644
--- a/pymsbuild/targets/cpp-POSIX_x64-GCC.props
+++ b/pymsbuild/targets/cpp-POSIX_x64-GCC.props
@@ -1,8 +1,31 @@
 <Project>
   <PropertyGroup>
-    <PythonConfig Condition="$(PythonConfig) == ''">$(HostPython)-config</PythonConfig>
+    <PythonConfig Condition="$(PythonConfig) == '' and Exists('$(HostPython)-config')">$(HostPython)-config</PythonConfig>
+    <PythonConfig Condition="$(PythonConfig) == '' and Exists('$(BaseHostPython)-config')">$(BaseHostPython)-config</PythonConfig>
+    <PythonConfig Condition="$(PythonConfig) == ''">python3-config</PythonConfig>
+  </PropertyGroup>
+  <PropertyGroup Condition="$(ConfigurationType) == 'Application'">
+    <CC_Cmd Condition="$(CC_Cmd) == ''">gcc -pthread -fPIE</CC_Cmd>
+    <CPP_Cmd Condition="$(CPP_Cmd) == ''">g++ -pthread -fPIE</CPP_Cmd>
+    <Link_Cmd Condition="$(Link_Cmd) == ''">gcc -pthread</Link_Cmd>
+    <TargetExt></TargetExt>
+  </PropertyGroup>
+  <PropertyGroup Condition="$(ConfigurationType) == 'DynamicLibrary'">
     <CC_Cmd Condition="$(CC_Cmd) == ''">gcc -pthread -fPIC</CC_Cmd>
     <CPP_Cmd Condition="$(CPP_Cmd) == ''">g++ -pthread -fPIC</CPP_Cmd>
     <Link_Cmd Condition="$(Link_Cmd) == ''">gcc -pthread -shared</Link_Cmd>
+    <TargetExt Condition="$(TargetExt) == ''">.so</TargetExt>
+  </PropertyGroup>
+  <PropertyGroup Condition="$(ConfigurationType) == 'ExtensionModule'">
+    <CC_Cmd Condition="$(CC_Cmd) == ''">gcc -pthread -fPIC</CC_Cmd>
+    <CPP_Cmd Condition="$(CPP_Cmd) == ''">g++ -pthread -fPIC</CPP_Cmd>
+    <Link_Cmd Condition="$(Link_Cmd) == ''">gcc -pthread -shared</Link_Cmd>
+    <TargetExt Condition="$(TargetExt) == ''">$(DefaultExtSuffix)</TargetExt>
+  </PropertyGroup>
+  <PropertyGroup Condition="$(ConfigurationType) == 'StaticLibrary'">
+    <CC_Cmd Condition="$(CC_Cmd) == ''">gcc -pthread -fPIC</CC_Cmd>
+    <CPP_Cmd Condition="$(CPP_Cmd) == ''">g++ -pthread -fPIC</CPP_Cmd>
+    <Link_Cmd Condition="$(Link_Cmd) == ''">ar rcs</Link_Cmd>
+    <TargetExt Condition="$(TargetExt) == ''">.a</TargetExt>
   </PropertyGroup>
 </Project>
diff --git a/pymsbuild/targets/cpp-POSIX_x64.targets b/pymsbuild/targets/cpp-POSIX_x64.targets
index 0e5b2a6..72e3079 100644
--- a/pymsbuild/targets/cpp-POSIX_x64.targets
+++ b/pymsbuild/targets/cpp-POSIX_x64.targets
@@ -28,11 +28,14 @@
 
   <Target Name="_CalculateFlags">
     <Message Text="Executing $(PythonConfig). If this fails, you may need to set PYTHON_CONFIG to the correct command." />
-    <Exec Command="$(PythonConfig) --cflags" ConsoleToMsBuild="true" Condition="$(PythonCFlags) == ''"
+    <PropertyGroup>
+      <_Embed Condition="$(ConfigurationType) == 'Application'">--embed</_Embed>
+    </PropertyGroup>
+    <Exec Command="$(PythonConfig) --cflags $(_Embed)" ConsoleToMsBuild="true" Condition="$(PythonCFlags) == ''"
           StandardOutputImportance="low">
       <Output TaskParameter="ConsoleOutput" PropertyName="PythonCFlags" />
     </Exec>
-    <Exec Command="$(PythonConfig) --ldflags" ConsoleToMsBuild="true" Condition="$(PythonLDFlags) == ''"
+    <Exec Command="$(PythonConfig) --ldflags $(_Embed)" ConsoleToMsBuild="true" Condition="$(PythonLDFlags) == ''"
           StandardOutputImportance="low">
       <Output TaskParameter="ConsoleOutput" PropertyName="PythonLDFlags" />
     </Exec>
@@ -106,7 +109,58 @@
   <Target Name="Link" DependsOnTargets="$(BeforeLinkTargets);_CalculateLinkerInputsItems;_Link">
   </Target>
 
-  <Target Name="CoreBuild" DependsOnTargets="_CalculateFlags;PrepareForBuild;$(BeforeBuildGenerateSourcesTargets);ClCompile;Link" />
+  <Target Name="GetCopyToOutputDirectoryItems"
+          DependsOnTargets="$(GetCopyToOutputDirectoryItemsDependsOn)"
+          Returns="@(CopyToOutputItems)">
+    <ItemGroup>
+      <CopyToOutputItems Include="@(Content)" Condition="%(Content.CopyToOutputDirectory) == 'Always'" />
+      <CopyToOutputItems_Update Include="@(Content)" Condition="%(Content.CopyToOutputDirectory) == 'PreserveNewest'" />
+
+      <CopyToOutputItems Include="@(None)" Condition="%(None.CopyToOutputDirectory) == 'Always'" />
+      <CopyToOutputItems_Update Include="@(None)" Condition="%(None.CopyToOutputDirectory) == 'PreserveNewest'" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_AssignCopyToOutputDirectoryItems">
+    <ItemGroup>
+      <CopyToOutputItems>
+        <_CopyTo Condition="%(TargetDir) != ''">$(OutDir)/%(TargetDir)/%(TargetNem)%(TargetExt)</_CopyTo>
+        <_CopyTo Condition="%(TargetDir) == ''">$(OutDir)/%(TargetNem)%(TargetExt)</_CopyTo>
+      </CopyToOutputItems>
+      <CopyToOutputItems_Update>
+        <_CopyTo Condition="%(TargetDir) != ''">$(OutDir)/%(TargetDir)/%(TargetNem)%(TargetExt)</_CopyTo>
+        <_CopyTo Condition="%(TargetDir) == ''">$(OutDir)/%(TargetNem)%(TargetExt)</_CopyTo>
+      </CopyToOutputItems_Update>
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_MaybeCopyToOutputDirectory"
+          Inputs="@(CopyToOutputItems_Update)"
+          Outputs="%(CopyToOutputItems_Update._CopyTo)">
+    <Copy SourceFiles="@(CopyToOutputItems_Update)"
+          DestinationFiles="%(CopyToOutputItems_Update._CopyTo)"
+          OverwriteReadOnlyFiles="true"
+          Retries="$(CopyRetryCount)" />
+    <ItemGroup>
+      <FileWrites Include="%(CopyToOutputItems_Update._CopyTo)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_ForceCopyToOutputDirectory">
+    <Copy SourceFiles="@(CopyToOutputItems)"
+          DestinationFiles="%(CopyToOutputItems._CopyTo)"
+          OverwriteReadOnlyFiles="true"
+          Retries="$(CopyRetryCount)" />
+    <ItemGroup>
+      <FileWrites Include="%(CopyToOutputItems._CopyTo)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_CopyToOutputDirectory"
+          DependsOnTargets="GetCopyToOutputDirectoryItems;_AssignCopyToOutputDirectoryItems;_MaybeCopyToOutputDirectory;_ForceCopyToOutputDirectory">
+  </Target>
+
+  <Target Name="CoreBuild" DependsOnTargets="_CalculateFlags;PrepareForBuild;$(BeforeBuildGenerateSourcesTargets);ClCompile;Link;_CopyToOutputDirectory" />
   <Target Name="Build" DependsOnTargets="CoreBuild;_SaveFileWrites" />
   <Target Name="Clean" DependsOnTargets="_CleanFileWrites">
     <RemoveDir Directories="$(IntDir)" />
diff --git a/pymsbuild/targets/cpp-Win32.props b/pymsbuild/targets/cpp-Win32.props
index d097527..5a9addf 100644
--- a/pymsbuild/targets/cpp-Win32.props
+++ b/pymsbuild/targets/cpp-Win32.props
@@ -1,3 +1,7 @@
 <Project>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" Condition="$(VCTargetsPath) != ''" />
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.WinDLL.props" Condition="$(ConfigurationType) == 'ExtensionModule' and $(VCTargetsPath) != ''" />
+  <PropertyGroup>
+    <TargetExt Condition="$(ConfigurationType) == 'ExtensionModule'">$(DefaultExtSuffix)</TargetExt>
+  </PropertyGroup>
 </Project>
diff --git a/pymsbuild/targets/cpp-Win32.targets b/pymsbuild/targets/cpp-Win32.targets
index ffbdc14..69e1919 100644
--- a/pymsbuild/targets/cpp-Win32.targets
+++ b/pymsbuild/targets/cpp-Win32.targets
@@ -1,10 +1,11 @@
 <Project>
   <Target Name="_GetPdbPackageFiles">
     <ItemGroup>
-      <AllSourceFiles Include="$(OutDir)$(TargetName).pdb" Condition="$(Configuration) == 'Debug'">
+      <_PdbFile Include="$([System.IO.Path]::ChangeExtension($(TargetPath), '.pdb'))" Condition="$(Configuration) == 'Debug'" />
+      <AllSourceFiles Include="@(_PdbFile)">
         <TargetDir></TargetDir>
-        <TargetName>$(TargetName)</TargetName>
-        <TargetExt>.pdb</TargetExt>
+        <TargetName>%(Filename)</TargetName>
+        <TargetExt>%(Extension)</TargetExt>
         <IncludeInWheel>true</IncludeInWheel>
         <IncludeInSdist>false</IncludeInSdist>
       </AllSourceFiles>
diff --git a/pymsbuild/targets/cpp-default-POSIX_x64.props b/pymsbuild/targets/cpp-default-POSIX_x64.props
index 604cd8d..d194559 100644
--- a/pymsbuild/targets/cpp-default-POSIX_x64.props
+++ b/pymsbuild/targets/cpp-default-POSIX_x64.props
@@ -1,5 +1,6 @@
 <Project>
   <PropertyGroup>
     <DefaultPlatformToolset>GCC</DefaultPlatformToolset>
+    <CopyRetryCount Condition="$(CopyRetryCount) == ''">3</CopyRetryCount>
   </PropertyGroup>
 </Project>
diff --git a/pymsbuild/targets/cpp-x64.props b/pymsbuild/targets/cpp-x64.props
index d097527..5a9addf 100644
--- a/pymsbuild/targets/cpp-x64.props
+++ b/pymsbuild/targets/cpp-x64.props
@@ -1,3 +1,7 @@
 <Project>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" Condition="$(VCTargetsPath) != ''" />
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.WinDLL.props" Condition="$(ConfigurationType) == 'ExtensionModule' and $(VCTargetsPath) != ''" />
+  <PropertyGroup>
+    <TargetExt Condition="$(ConfigurationType) == 'ExtensionModule'">$(DefaultExtSuffix)</TargetExt>
+  </PropertyGroup>
 </Project>
diff --git a/pymsbuild/targets/cpp-x64.targets b/pymsbuild/targets/cpp-x64.targets
index ffbdc14..69e1919 100644
--- a/pymsbuild/targets/cpp-x64.targets
+++ b/pymsbuild/targets/cpp-x64.targets
@@ -1,10 +1,11 @@
 <Project>
   <Target Name="_GetPdbPackageFiles">
     <ItemGroup>
-      <AllSourceFiles Include="$(OutDir)$(TargetName).pdb" Condition="$(Configuration) == 'Debug'">
+      <_PdbFile Include="$([System.IO.Path]::ChangeExtension($(TargetPath), '.pdb'))" Condition="$(Configuration) == 'Debug'" />
+      <AllSourceFiles Include="@(_PdbFile)">
         <TargetDir></TargetDir>
-        <TargetName>$(TargetName)</TargetName>
-        <TargetExt>.pdb</TargetExt>
+        <TargetName>%(Filename)</TargetName>
+        <TargetExt>%(Extension)</TargetExt>
         <IncludeInWheel>true</IncludeInWheel>
         <IncludeInSdist>false</IncludeInSdist>
       </AllSourceFiles>
diff --git a/pymsbuild/targets/dllpack-generate.py b/pymsbuild/targets/dllpack-generate.py
index 5fab672..9905b3d 100644
--- a/pymsbuild/targets/dllpack-generate.py
+++ b/pymsbuild/targets/dllpack-generate.py
@@ -9,7 +9,7 @@
 except ValueError:
     PYC_OPTIMIZATION = 0
 
-RESID_COUNTER = iter(range(1001, 9999))
+RESID_COUNTER = iter(range(1001, 999999))
 IMPORTERS_RESID = next(RESID_COUNTER)
 
 
@@ -99,8 +99,8 @@ def check(self):
             return "Missing input: {}".format(self.sourcefile)
 
     @classmethod
-    def get_builtin(cls, resid, sourcefile):
-        return cls("code:${}:{}".format(sourcefile.stem, sourcefile), resid=resid)
+    def get_builtin(cls, resid, sourcefile, name):
+        return cls("code:${}:{}".format(name or sourcefile.stem, sourcefile), resid=resid)
 
 
 class DataFileInfo:
@@ -275,7 +275,9 @@ def _c_str(s):
 
 
 def _generate_windows_files(module, files, targets, encrypt=None):
-    files.append(CodeFileInfo.get_builtin(IMPORTERS_RESID, targets / "dllpack_main.py"))
+    files.append(CodeFileInfo.get_builtin(IMPORTERS_RESID, targets / "dllpack_main.py", f"dllpack.{module}"))
+
+    module_name = module.rpartition(".")[2]
 
     with open("dllpack.rc", "w", encoding="ascii", errors="backslashescape") as rc_file:
         print("#define PYCFILE 257", file=rc_file)
@@ -286,7 +288,7 @@ def _generate_windows_files(module, files, targets, encrypt=None):
 
     with open("dllpack.h", "w", encoding="ascii", errors="backslashescape") as h_file:
         print('#define _MODULE_NAME "{}"'.format(module), file=h_file)
-        print('#define _INIT_FUNC_NAME PyInit_{}'.format(module), file=h_file)
+        print('#define _INIT_FUNC_NAME PyInit_{}'.format(module_name), file=h_file)
         print("#define _PYCFILE 257", file=h_file)
         print("#define _DATAFILE 258", file=h_file)
         print("#define _PYC_HEADER_LEN 16", file=h_file)
@@ -320,9 +322,11 @@ def _generate_windows_files(module, files, targets, encrypt=None):
 
 
 def _generate_gcc_files(module, files, targets, encrypt=None):
-    importer = CodeFileInfo.get_builtin(IMPORTERS_RESID, targets / "dllpack_main.py")
+    importer = CodeFileInfo.get_builtin(IMPORTERS_RESID, targets / "dllpack_main.py", f"dllpack.{module}")
     files.append(importer)
 
+    module_name = module.rpartition(".")[2]
+
     with open("dllpack.rc", "w", encoding="utf-8", errors="strict") as rc_file:
         for f in files:
             if f.RC_TYPE:
@@ -330,7 +334,7 @@ def _generate_gcc_files(module, files, targets, encrypt=None):
 
     with open("dllpack.h", "w", encoding="ascii", errors="backslashescape") as h_file:
         print('#define _MODULE_NAME "{}"'.format(module), file=h_file)
-        print('#define _INIT_FUNC_NAME PyInit_{}'.format(module), file=h_file)
+        print('#define _INIT_FUNC_NAME PyInit_{}'.format(module_name), file=h_file)
         print("#define _PYC_HEADER_LEN 16", file=h_file)
         print('#include "dllpack-gcc.h"', file=h_file)
         if encrypt:
diff --git a/pymsbuild/targets/dllpack.c b/pymsbuild/targets/dllpack.c
index b820e11..7b59226 100644
--- a/pymsbuild/targets/dllpack.c
+++ b/pymsbuild/targets/dllpack.c
@@ -3,27 +3,67 @@
 
 #include "dllpack.h"
 
+#define MATCH_NONE 0
+#define MATCH_FULL 1
+#define MATCH_FULL_PACKAGE 2
+#define MATCH_PARENT 3
+#define MATCH_PREFIX 4
+
+static int
+entry_matches(const struct ENTRY *entry, const char *name, size_t cchName)
+{
+    if (!cchName) {
+        cchName = strlen(name);
+    }
+    if (!cchName) {
+        if (strchr(entry->name, '.')) {
+            return MATCH_PARENT;
+        }
+        return MATCH_NONE;
+    }
+
+    if (!strcmp(entry->name, name)) {
+        // Full name match
+        if (entry->origin[cchName] == '/' || entry->origin[cchName] == '\\'){
+            // Origin is a directory, which means this is a package
+            return MATCH_FULL_PACKAGE;
+        }
+        return MATCH_FULL;
+    }
+    if (strncmp(entry->name, name, cchName) || entry->name[cchName] != '.') {
+        return MATCH_NONE;
+    }
+    // Prefix match
+    if (!strchr(&entry->name[cchName + 1], '.')) {
+        // name is direct parent of this entry
+        return MATCH_PARENT;
+    }
+    return MATCH_PREFIX;
+}
+
 static const struct ENTRY *
 lookup_import(const char *name, int *is_package)
 {
-    struct ENTRY *found = NULL;
     int cchName = strlen(name);
     *is_package = 0;
     if (PySys_Audit("pymsbuild.dllpack.lookup_import", "ss", _DLLPACK_NAME, name) < 0) {
         return NULL;
     }
     for (struct ENTRY *entry = IMPORT_TABLE; entry->name; ++entry) {
-        if (!strcmp(entry->name, name)) {
-            found = entry;
-        }
-        if (!strncmp(entry->name, name, cchName) && entry->name[cchName] == '.') {
+        switch (entry_matches(entry, name, cchName)) {
+        case MATCH_FULL:
+            *is_package = 0;
+            return entry;
+        case MATCH_FULL_PACKAGE:
             *is_package = 1;
-            if (found) {
-                break;
-            }
+            return entry;
+        case MATCH_PARENT:
+        case MATCH_PREFIX:
+            *is_package = 1;
+            return NULL;
         }
     }
-    return found;
+    return NULL;
 }
 
 static const struct ENTRY *
@@ -56,10 +96,10 @@ lookup_redirect(const char *name)
 
 
 static PyObject *
-mod_load_impl(ModuleState *ms, const char *name, PyObject *mod, int is_main) {
+mod_exec_module_impl(ModuleState *ms, const char *name, PyObject *mod, int is_main) {
     int is_package = 0;
     const struct ENTRY *e = lookup_import(name, &is_package);
-    if (!e) {
+    if (!e && !is_package) {
         PyErr_Format(PyExc_ModuleNotFoundError, "'%s' is not part of this package", name);
         return NULL;
     }
@@ -78,6 +118,10 @@ mod_load_impl(ModuleState *ms, const char *name, PyObject *mod, int is_main) {
         Py_DECREF(r);
     }
 
+    if (!e && is_package) {
+        return mod;
+    }
+
     pyc = load_pyc(ms, e);
     if (!pyc) {
         return NULL;
@@ -104,20 +148,20 @@ mod_load_impl(ModuleState *ms, const char *name, PyObject *mod, int is_main) {
 }
 
 static PyObject *
-mod_load(PyObject *self, PyObject *args)
+mod_exec_module(PyObject *self, PyObject *args)
 {
     PyObject *mod = NULL;
     if (!PyArg_ParseTuple(args, "O", &mod)) {
         return NULL;
     }
-    mod = mod_load_impl((ModuleState*)PyModule_GetState(self), PyModule_GetName(mod), mod, 0);
+    mod = mod_exec_module_impl((ModuleState*)PyModule_GetState(self), PyModule_GetName(mod), mod, 0);
     Py_XINCREF(mod);
     return mod;
 }
 
 
 static PyObject *
-mod_new(PyObject *self, PyObject *args)
+mod_create_module(PyObject *self, PyObject *args)
 {
     PyObject *spec;
     if (!PyArg_ParseTuple(args, "O", &spec)) {
@@ -194,10 +238,11 @@ static PyObject *
 mod_makespec(PyObject *self, PyObject *args)
 {
     const char *name;
-    PyObject *loader;
+    PyObject *loader, *path_prefix;
     PyObject *ilib_m = NULL, *mspec = NULL, *kwargs = NULL, *origin = NULL, *r = NULL;
+    PyObject *args2 = NULL;
 
-    if (!PyArg_ParseTuple(args, "sO", &name, &loader)) {
+    if (!PyArg_ParseTuple(args, "sOO", &name, &loader, &path_prefix)) {
         return NULL;
     }
 
@@ -224,17 +269,16 @@ mod_makespec(PyObject *self, PyObject *args)
         if (PyErr_Occurred()) {
             goto error;
         }
-        e = lookup_redirect(name);
-        if (!e) {
-            if (!PyErr_Occurred()) {
-                r = Py_None;
-                Py_INCREF(r);
+        if (!is_package) {
+            e = lookup_redirect(name);
+            if (!e) {
+                if (!PyErr_Occurred()) {
+                    r = Py_None;
+                    Py_INCREF(r);
+                }
+                goto error;
             }
-            goto error;
-        }
-        args = Py_BuildValue("sO", name, Py_None);
-        if (!args) {
-            goto error;
+            Py_SETREF(loader, Py_None);
         }
     }
     origin = PyObject_GetAttrString(self, "_origin_root");
@@ -251,7 +295,11 @@ mod_makespec(PyObject *self, PyObject *args)
         }
     }
     if (origin) {
-        Py_SETREF(origin, PyUnicode_FromFormat("%U%s", origin, e->origin));
+        if (e) {
+            Py_SETREF(origin, PyUnicode_FromFormat("%U%s", origin, e->origin));
+        } else {
+            Py_SETREF(origin, PyUnicode_FromFormat("%U%s", origin, name));
+        }
     }
     if (!origin) {
         goto error;
@@ -259,19 +307,95 @@ mod_makespec(PyObject *self, PyObject *args)
     if (PyDict_SetItemString(kwargs, "origin", origin) < 0) {
         goto error;
     }
+
+    if (is_package && PyDict_SetItemString(kwargs, "is_package", Py_True) < 0) {
+        goto error;
+    }
+
+    args2 = Py_BuildValue("sO", name, loader);
+    if (!args2) {
+        goto error;
+    }
+
+    r = PyObject_Call(mspec, args2, kwargs);
+
     if (is_package) {
-        if (PyDict_SetItemString(kwargs, "is_package", Py_True) < 0) {
+        PyObject *paths = PyList_New(0);
+        if (!paths) {
+            Py_CLEAR(r);
+            goto error;
+        }
+        if (PyObject_IsTrue(path_prefix) && PyList_Append(paths, path_prefix) < 0) {
+            Py_DECREF(paths);
+            Py_CLEAR(r);
+            goto error;
+        }
+        if (PyObject_SetAttrString(r, "submodule_search_locations", paths) < 0) {
+            Py_DECREF(paths);
+            Py_CLEAR(r);
             goto error;
         }
+        Py_DECREF(paths);
     }
-
-    r = PyObject_Call(mspec, args, kwargs);
 error:
     Py_XDECREF(origin);
     Py_XDECREF(kwargs);
     Py_XDECREF(mspec);
     Py_XDECREF(ilib_m);
+    Py_XDECREF(args2);
+
+    return r;
+}
+
+static int
+_mod_module_names_append(PyObject *list, struct ENTRY *entry, const char *prefix, size_t cchPrefix)
+{
+    if (entry->name[0] == '.') {
+        return 0;
+    }
+    int cchName = strlen(entry->name);
+    int is_package = entry_matches(entry, entry->name, cchName) == MATCH_FULL_PACKAGE;
+    if (!cchPrefix) {
+        cchPrefix = strlen(prefix);
+    }
+    if (cchPrefix) {
+        cchPrefix += 1;
+    }
+    PyObject *o = Py_BuildValue("si", &entry->name[cchPrefix], is_package);
+    int r = -1;
+    if (o) {
+        r = PyList_Append(list, o);
+        Py_DECREF(o);
+    }
+    return r;
+}
+
 
+static PyObject *
+mod_module_names(PyObject *self, PyObject *args)
+{
+    const char *prefix;
+    if (!PyArg_ParseTuple(args, "s", &prefix)) {
+        return NULL;
+    }
+    size_t cchPrefix = strlen(prefix);
+    if (PySys_Audit("pymsbuild.dllpack.module_names", "ss", _DLLPACK_NAME, prefix) < 0) {
+        return NULL;
+    }
+    PyObject *r = PyList_New(0);
+    if (!r) {
+        return NULL;
+    }
+    for (struct ENTRY *entry = IMPORT_TABLE; entry->name; ++entry) {
+        switch (entry_matches(entry, prefix, cchPrefix)) {
+        case MATCH_PARENT:
+            if (_mod_module_names_append(r, entry, prefix, cchPrefix) < 0) {
+                Py_DECREF(r);
+                return NULL;
+            }
+            break;
+        }
+    }
     return r;
 }
 
@@ -334,7 +458,7 @@ mod_exec(PyObject *m)
         return -1;
     if (dllpack_exec_module(ms) < 0)
         return -1;
-    return mod_load_impl(ms, PyModule_GetName(m), m, 1) ? 0 : -1;
+    return mod_exec_module_impl(ms, PyModule_GetName(m), m, 1) ? 0 : -1;
 }
 
 
@@ -352,8 +476,9 @@ static struct PyMethodDef mod_meth[] = {
     {"__MAKESPEC", mod_makespec, METH_VARARGS, NULL},
     {"__DATA", mod_data, METH_VARARGS, NULL},
     {"__DATA_NAMES", mod_data_names, METH_NOARGS, NULL},
-    {"__CREATE_MODULE", mod_new, METH_VARARGS, NULL},
-    {"__EXEC_MODULE", mod_load, METH_VARARGS, NULL},
+    {"__CREATE_MODULE", mod_create_module, METH_VARARGS, NULL},
+    {"__EXEC_MODULE", mod_exec_module, METH_VARARGS, NULL},
+    {"__MODULE_NAMES", mod_module_names, METH_VARARGS, NULL},
     MOD_METH_TAIL
 };
 
diff --git a/pymsbuild/targets/dllpack_main.py b/pymsbuild/targets/dllpack_main.py
index 93a1b75..5ac1b6f 100644
--- a/pymsbuild/targets/dllpack_main.py
+++ b/pymsbuild/targets/dllpack_main.py
@@ -1,10 +1,7 @@
 def _init():
     import sys
-
-    from importlib import import_module
-    from importlib.abc import Loader, MetaPathFinder, ResourceReader
-    from importlib.machinery import ExtensionFileLoader, ModuleSpec
-    from ntpath import split as nt_split
+    from importlib.abc import Loader, MetaPathFinder, PathEntryFinder, ResourceReader
+    from importlib.machinery import ExtensionFileLoader
 
     _NAME = __NAME()
     _NAME_DOT = _NAME + "."
@@ -13,6 +10,7 @@ def _init():
     _DATA_NAMES = set(__DATA_NAMES())
     _CREATE_MODULE = __CREATE_MODULE
     _EXEC_MODULE = __EXEC_MODULE
+    _MODULE_NAMES = __MODULE_NAMES
 
     class DllPackReader(ResourceReader):
         def __init__(self, prefix):
@@ -42,7 +40,7 @@ class DllPackLoader(Loader):
 
         def get_resource_reader(self, fullname):
             try:
-                spec = _MAKESPEC(fullname, LOADER)
+                spec = _MAKESPEC(fullname, LOADER, None)
                 if spec.submodule_search_locations is None:
                     return None
             except Exception:
@@ -54,29 +52,44 @@ def get_resource_reader(self, fullname):
 
     LOADER = DllPackLoader()
 
-
     class DllPackFinder(MetaPathFinder):
-        def find_spec(self, fullname, path, target=None):
+        _PATH_HOOK_PREFIX = "$dllpack:" + _NAME
+
+        def __init__(self, prefix):
+            self._prefix = prefix
+
+        @classmethod
+        def find_spec(cls, fullname, path=None, target=None):
             if fullname.startswith(_NAME_DOT) or fullname == _NAME:
-                spec = _MAKESPEC(fullname, LOADER)
+                spec = _MAKESPEC(fullname, LOADER, "$dllpack:" + fullname)
                 if spec and not spec.loader:
                     spec.loader = ExtensionFileLoader(spec.name, spec.origin)
                 return spec
 
+        def iter_modules(self, prefix):
+            if not prefix:
+                prefix = self._prefix
+            return _MODULE_NAMES(prefix)
+
+        @classmethod
+        def hook(cls, path):
+            if path.startswith(cls._PATH_HOOK_PREFIX):
+                return cls(path[len("$dllpack:"):])
+            raise ImportError()
+
     DllPackFinder.__name__ += "_" + _NAME
     DllPackFinder.__qualname__ = "<generated>." + DllPackFinder.__name__
 
-    FINDER = next((getattr(m, "__name__", None) == DllPackFinder.__name__ for m in sys.meta_path), None)
-    if not FINDER:
-        FINDER = DllPackFinder()
-        sys.meta_path.insert(0, FINDER)
+    if not any(getattr(m, "__name__", None) == DllPackFinder.__name__ for m in sys.meta_path):
+        sys.meta_path.insert(0, DllPackFinder)
+        sys.path_hooks.append(DllPackFinder.hook)
 
-    return _MAKESPEC(__name__, LOADER)
+    return _MAKESPEC(__name__, LOADER, DllPackFinder._PATH_HOOK_PREFIX)
 
 
 __spec__ = _init()
 __file__ = __spec__.origin
 __loader__ = __spec__.loader
 __package__ = getattr(__spec__, "parent", None)
-__path__ = []
-del _init, __CREATE_MODULE, __DATA, __DATA_NAMES, __EXEC_MODULE, __MAKESPEC, __NAME
+__path__ = __spec__.submodule_search_locations
+del _init, __CREATE_MODULE, __DATA, __DATA_NAMES, __EXEC_MODULE, __MODULE_NAMES, __MAKESPEC, __NAME
diff --git a/pymsbuild/targets/entrypoint-gcc.c b/pymsbuild/targets/entrypoint-gcc.c
new file mode 100644
index 0000000..a62ebba
--- /dev/null
+++ b/pymsbuild/targets/entrypoint-gcc.c
@@ -0,0 +1,104 @@
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+
+#define _GNU_SOURCE 1
+#include <unistd.h>
+
+#define PYTHONPATH_T const char *
+#define PYTHONPATH_ENTRY(s) s
+#include "entrypoint.h"
+
+#define CHECK_STATUS(op) status = op; \
+if (PyStatus_Exception(status)) { \
+    Py_ExitStatusException(status); \
+    return -1; \
+}
+
+
+int main(int argc, char **argv)
+{
+    PyStatus status;
+    PyPreConfig preconfig;
+    PyConfig config;
+    const size_t maxPath = 32678;
+    char executable[maxPath];
+    char home[maxPath];
+    char *progname;
+
+    if (argv[0][0] == '/' || !getcwd(executable, maxPath)) {
+        strcpy(executable, argv[0]);
+    } else {
+        if (executable[strlen(executable) -1] != '/') {
+            strcat(executable, "/");
+        }
+        strcat(executable, argv[0]);
+    }
+    strcpy(home, executable);
+    strcat(executable, ".donotexecute");
+    progname = strrchr(home, '/');
+    if (progname) {
+        *progname++ = '\0';
+    }
+
+    PyPreConfig_InitIsolatedConfig(&preconfig);
+#ifdef _ENTRYPOINT_UTF8MODE
+    preconfig.utf8_mode = _ENTRYPOINT_UTF8MODE;
+#endif
+
+    CHECK_STATUS(Py_PreInitialize(&preconfig));
+    PyConfig_InitIsolatedConfig(&config);
+    CHECK_STATUS(PyConfig_SetBytesArgv(&config, argc, argv));
+#if PY_HEXVERSION >= 0x030B0000
+    config.code_debug_ranges = 0;
+#endif
+    config.configure_c_stdio = 1;
+    PyConfig_SetBytesString(&config, &config.exec_prefix, home);
+    PyConfig_SetBytesString(&config, &config.executable, executable);
+    PyConfig_SetBytesString(&config, &config.home, home);
+    PyConfig_SetBytesString(&config, &config.prefix, home);
+    if (progname) {
+        PyConfig_SetBytesString(&config, &config.program_name, progname);
+    }
+#ifdef _ENTRYPOINT_IMPORTTIME
+    config.import_time = _ENTRYPOINT_IMPORTTIME;
+#endif
+    config.install_signal_handlers = 1;
+
+    for (PYTHONPATH_T *p = entrypointPythonPath; *p; ++p) {
+        char searchPath[maxPath];
+        if (*p[0] == '/') {
+            strcpy(searchPath, *p);
+        } else if (strcmp(*p, ".")) {
+            snprintf(searchPath, maxPath, "%s/%s", home, *p);
+        } else {
+            strcpy(searchPath, home);
+        }
+        const wchar_t *wpath = Py_DecodeLocale(searchPath, NULL);
+        if (wpath) {
+            CHECK_STATUS(PyWideStringList_Append(&config.module_search_paths, wpath));
+            PyMem_RawFree((void *)wpath);
+        } else {
+            fprintf(stderr, "WARN: failed to add search path %s\n", *p);
+        }
+    }
+    config.module_search_paths_set = 1;
+    config.optimization_level = 2;
+    config.site_import = 0;
+#if PY_HEXVERSION >= 0x030C0000
+    config.perf_profiling = 0;
+#endif
+    config.write_bytecode = 0;
+
+    CHECK_STATUS(Py_InitializeFromConfig(&config));
+
+    PyObject *mod = PyImport_ImportModule(entrypointModule);
+    PyObject *r = mod ? PyObject_CallMethod(mod, entrypointFunction, NULL) : NULL;
+    if (!r) {
+        PyErr_Print();
+        return 1;
+    }
+    Py_DECREF(r);
+    Py_DECREF(mod);
+    Py_Finalize();
+    return 0;
+}
diff --git a/pymsbuild/targets/entrypoint-windows.cpp b/pymsbuild/targets/entrypoint-windows.cpp
new file mode 100644
index 0000000..0f1334b
--- /dev/null
+++ b/pymsbuild/targets/entrypoint-windows.cpp
@@ -0,0 +1,97 @@
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+
+#include <windows.h>
+#include <pathcch.h>
+#pragma comment(lib, "pathcch.lib")
+
+#define PYTHONPATH_T const wchar_t *
+#define PYTHONPATH_ENTRY(s) L ## s
+#include "entrypoint.h"
+
+#define CHECK_STATUS(op) status = op; \
+if (PyStatus_Exception(status)) { \
+    Py_ExitStatusException(status); \
+    return -1; \
+}
+
+
+int wmain(int argc, wchar_t **argv)
+{
+    PyStatus status;
+    PyPreConfig preconfig;
+    PyConfig config;
+    const size_t maxPath = 32678;
+    wchar_t executable[maxPath];
+    wchar_t home[maxPath];
+    wchar_t *progname;
+    if (!GetModuleFileNameW(NULL, executable, maxPath) ||
+        wcscpy_s(home, maxPath, executable) ||
+        FAILED(PathCchRemoveFileSpec(home, maxPath))) {
+        executable[0] = L'\0';
+        home[0] = L'\0';
+    }
+    progname = &executable[wcslen(home)];
+    if (executable[0]) {
+        wcscat_s(executable, maxPath, L".donotexecute");
+    }
+    if (*progname == L'\\') ++progname;
+
+    PyPreConfig_InitIsolatedConfig(&preconfig);
+#ifdef _ENTRYPOINT_UTF8MODE
+    preconfig.utf8_mode = _ENTRYPOINT_UTF8MODE;
+#endif
+
+    CHECK_STATUS(Py_PreInitialize(&preconfig));
+    PyConfig_InitIsolatedConfig(&config);
+    CHECK_STATUS(PyConfig_SetArgv(&config, argc, argv));
+#if PY_HEXVERSION >= 0x030B0000
+    config.code_debug_ranges = 0;
+#endif
+    config.configure_c_stdio = 1;
+    PyConfig_SetString(&config, &config.exec_prefix, home);
+    PyConfig_SetString(&config, &config.executable, executable);
+    PyConfig_SetString(&config, &config.home, home);
+    PyConfig_SetString(&config, &config.prefix, home);
+    PyConfig_SetString(&config, &config.program_name, progname);
+#ifdef _ENTRYPOINT_IMPORTTIME
+    config.import_time = _ENTRYPOINT_IMPORTTIME;
+#endif
+    config.install_signal_handlers = 1;
+
+    for (PYTHONPATH_T *p = entrypointPythonPath; *p; ++p) {
+        wchar_t searchPath[maxPath];
+        searchPath[0] = L'\0';
+        if (FAILED(PathCchCombineEx(searchPath, maxPath, home, *p, PATHCCH_ALLOW_LONG_PATHS))) {
+            fprintf(stderr, "WARN: failed to add search path %S\n", *p);
+        } else {
+            CHECK_STATUS(PyWideStringList_Append(&config.module_search_paths, searchPath));
+        }
+    }
+    config.module_search_paths_set = 1;
+    config.optimization_level = 2;
+    config.site_import = 0;
+#if PY_HEXVERSION >= 0x030C0000
+    config.perf_profiling = 0;
+#endif
+    config.write_bytecode = 0;
+
+    CHECK_STATUS(Py_InitializeFromConfig(&config));
+
+    PyObject *mod = PyImport_ImportModule(entrypointModule);
+    PyObject *r;
+    if (entrypointFunction && entrypointFunction[0]) {
+        r = mod ? PyObject_CallMethod(mod, entrypointFunction, NULL) : NULL;
+    } else {
+        r = mod ? Py_None : NULL;
+        Py_XINCREF(r);
+    }
+    if (!r) {
+        PyErr_Print();
+        return 1;
+    }
+    Py_DECREF(r);
+    Py_DECREF(mod);
+    Py_Finalize();
+    return 0;
+}
diff --git a/pymsbuild/targets/entrypoint-windows.rc b/pymsbuild/targets/entrypoint-windows.rc
new file mode 100644
index 0000000..c24bdaa
--- /dev/null
+++ b/pymsbuild/targets/entrypoint-windows.rc
@@ -0,0 +1,5 @@
+#include <windows.h>
+
+#ifdef _ENTRYPOINT_ICON
+1 ICON _ENTRYPOINT_ICON
+#endif
diff --git a/pymsbuild/targets/entrypoint.props b/pymsbuild/targets/entrypoint.props
new file mode 100644
index 0000000..d6d3c29
--- /dev/null
+++ b/pymsbuild/targets/entrypoint.props
@@ -0,0 +1,11 @@
+<Project>
+  <PropertyGroup>
+    <ConfigurationType>Application</ConfigurationType>
+
+    <EntrypointModule Condition="$(EntrypointModule) == ''">app</EntrypointModule>
+    <EntrypointFunction Condition="$(EntrypointFunction) == ''">main</EntrypointFunction>
+
+    <PythonRuntimeEmbeddable Condition="$(PythonRuntimeEmbeddable) == ''">true</PythonRuntimeEmbeddable>
+    <PythonRuntimeExecutables Condition="$(PythonRuntimeExecutables) == ''">false</PythonRuntimeExecutables>
+  </PropertyGroup>
+</Project>
diff --git a/pymsbuild/targets/entrypoint.targets b/pymsbuild/targets/entrypoint.targets
new file mode 100644
index 0000000..265e33c
--- /dev/null
+++ b/pymsbuild/targets/entrypoint.targets
@@ -0,0 +1,120 @@
+<Project>
+  <PropertyGroup>
+    <_EntrypointSource Condition="$(PlatformToolset) == 'gcc'">$(MSBuildThisFileDirectory)/entrypoint-gcc.c</_EntrypointSource>
+    <_EntrypointSource Condition="$(PlatformToolset) != 'gcc'">$(MSBuildThisFileDirectory)/entrypoint-windows.cpp</_EntrypointSource>
+    <_EntrypointResource Condition="$(PlatformToolset) != 'gcc'">$(MSBuildThisFileDirectory)/entrypoint-windows.rc</_EntrypointResource>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <GetSdistFilesTargets>$(GetSdistFilesTargets)</GetSdistFilesTargets>
+    <GetPackageFilesTargets>_GetEntryPointPackageFiles;$(GetPackageFilesTargets)</GetPackageFilesTargets>
+    <BeforeBuildGenerateSourcesTargets>_UpdateEntrypointSources;$(BeforeBuildGenerateSourcesTargets)</BeforeBuildGenerateSourcesTargets>
+  </PropertyGroup>
+
+  <Target Name="_UpdateDefaultEntrypointPythonPath_Windows"
+          DependsOnTargets="_DownloadRuntime_Properties"
+          Condition="$(PlatformToolset) != 'gcc'">
+    <ItemGroup Condition="$(IncludePythonRuntime) != 'false' and $(PythonRuntimeEmbeddable) and $(PythonRuntimeVersionless)">
+      <EntrypointPythonPath Include="stdlib.zip" />
+    </ItemGroup>
+    <ItemGroup Condition="$(IncludePythonRuntime) != 'false' and $(PythonRuntimeEmbeddable) and !$(PythonRuntimeVersionless)">
+      <EntrypointPythonPath Include="python*.zip" />
+    </ItemGroup>
+    <Error Condition="$(PythonRuntimeEmbeddable) and !$(PythonRuntimeVersionless)"
+           Text="DefaultSearchPath is not supported without PythonRuntimeVersionless=true" />
+    <ItemGroup Condition="$(IncludePythonRuntime) == 'false'">
+      <EntrypointPythonPath Include="$([System.IO.Path]::Combine($(PythonLibs), '..\Lib'))" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_UpdateDefaultEntrypointPythonPath_Gcc"
+          DependsOnTargets="_DownloadRuntime_Properties"
+          Condition="$(PlatformToolset) == 'gcc'">
+    <!-- TODO: Update when enabling download of runtime -->
+    <Exec Command="$(PythonConfig) --configdir"
+          ConsoleToMsBuild="true"
+          StandardOutputImportance="low">
+      <Output TaskParameter="ConsoleOutput" PropertyName="_ConfigDir" />
+    </Exec>
+    <Exec Command="make -f $(_ConfigDir)/Makefile -E 'print:;@printf %24(LIBDEST)\\n%24(DESTSHARED)\\n' print 2>nul"
+          ConsoleToMsBuild="true"
+          StandardOutputImportance="low">
+      <Output TaskParameter="ConsoleOutput" ItemName="_LibDest" />
+    </Exec>
+    <ItemGroup>
+      <EntrypointPythonPath Include="@(_LibDest)" Condition="Exists(%(Identity))" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_UpdateDefaultEntrypointPythonPath"
+          DependsOnTargets="_UpdateDefaultEntrypointPythonPath_Windows;_UpdateDefaultEntrypointPythonPath_Gcc"
+          Condition="@(DefaultEntrypointPythonPath) != '' or @(EntrypointPythonPath) == ''">
+  </Target>
+
+  <Target Name="_UpdateEntrypointSources" DependsOnTargets="_UpdateDefaultEntrypointPythonPath">
+    <PropertyGroup Condition="@(EntrypointIcon) != ''">
+      <_EntrypointDef>$(_EntrypointDef);_ENTRYPOINT_ICON=@(EntrypointIcon)</_EntrypointDef>
+    </PropertyGroup>
+
+    <PropertyGroup>
+      <_EntrypointH>$(IntDir)/entrypoint.h</_EntrypointH>
+      <_OriginalCode Condition="Exists($(_EntrypointH))">$([System.IO.File]::ReadAllText($(_EntrypointH)))</_OriginalCode>
+      <_EntrypointPaths>@(EntrypointPythonPath->'    PYTHONPATH_ENTRY("%(Identity)"),', '%0A')</_EntrypointPaths>
+      <_NewCode>const char *entrypointModule = "$(EntrypointModule)"%3B
+const char *entrypointFunction = "$(EntrypointFunction)"%3B
+PYTHONPATH_T entrypointPythonPath[] = {
+$(_EntrypointPaths)
+    NULL
+}%3B
+</_NewCode>
+    </PropertyGroup>
+
+    <WriteLinesToFile File="$(_EntrypointH)" Lines="$(_NewCode)"
+                      Overwrite="true"
+                      Condition="$(_NewCode) != $(_OldCode)" />
+    <ItemGroup>
+      <ClInclude Include="$(_EntrypointH)" />
+      <FileWrites Include="$(_EntrypointH)" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <ClCompile Include="$(_EntrypointSource)">
+        <AdditionalIncludeDirectories>$(IntDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+        <PreprocessorDefinitions>%(PreprocessorDefinitions);$(_EntrypointDef)</PreprocessorDefinitions>
+        <ObjectFile Condition="$(PlatformToolset) == 'gcc'">$([msbuild]::EnsureTrailingSlash($(IntDir)))entrypoint.o</ObjectFile>
+        <ObjectFile Condition="$(PlatformToolset) != 'gcc'">$([msbuild]::EnsureTrailingSlash($(IntDir)))entrypoint.obj</ObjectFile>
+      </ClCompile>
+    </ItemGroup>
+    <ItemGroup Condition="$(_EntrypointResource) != ''">
+      <ResourceCompile Include="$(_EntrypointResource)">
+        <PreprocessorDefinitions>%(PreprocessorDefinitions);$(_EntrypointDef)</PreprocessorDefinitions>
+      </ResourceCompile>
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_EntryPoint_DownloadRuntime"
+          DependsOnTargets="DownloadRuntime"
+          Condition="$(IncludePythonRuntime) != 'false'" />
+
+  <Target Name="_GetEntryPointPackageFiles"
+          DependsOnTargets="_EntryPoint_DownloadRuntime">
+    <CallTarget Targets="GetCopyToOutputDirectoryItems">
+      <Output TaskParameter="TargetOutputs" ItemName="_GetCopyToOutputDirectoryItemsOutput"/>
+    </CallTarget>
+
+    <ItemGroup>
+      <AllSourceFiles Include="@(_GetCopyToOutputDirectoryItemsOutput)">
+        <Name>%(TargetPath)</Name>
+        <IncludeInSdist>false</IncludeInSdist>
+        <IncludeInLayout>true</IncludeInLayout>
+        <IncludeInWheel>true</IncludeInWheel>
+      </AllSourceFiles>
+      <AllSourceFiles Include="@(PythonRuntime)" Condition="$(IncludePythonRuntime) != 'false'">
+        <IncludeInSdist>false</IncludeInSdist>
+        <IncludeInLayout>true</IncludeInLayout>
+        <IncludeInWheel>true</IncludeInWheel>
+      </AllSourceFiles>
+    </ItemGroup>
+  </Target>
+  <Import Project="$(PyMSBuildTargets)/runtime.targets" />
+</Project>
diff --git a/pymsbuild/targets/package.props b/pymsbuild/targets/package.props
index 8c119d5..41b3996 100644
--- a/pymsbuild/targets/package.props
+++ b/pymsbuild/targets/package.props
@@ -1,2 +1,6 @@
 <Project>
+  <PropertyGroup>
+    <_SourceLayoutDir Condition="$(_PYMSBUILD_SOURCE_LAYOUT_DIR) != ''">$(_PYMSBUILD_SOURCE_LAYOUT_DIR)</_SourceLayoutDir>
+    <_SourceLayoutDir Condition="$(_SourceLayoutDir) == ''">$(SourceDir)</_SourceLayoutDir>
+  </PropertyGroup>
 </Project>
diff --git a/pymsbuild/targets/package.targets b/pymsbuild/targets/package.targets
index 8e024c6..2970a7c 100644
--- a/pymsbuild/targets/package.targets
+++ b/pymsbuild/targets/package.targets
@@ -15,12 +15,6 @@
         </PyMSBuild_Properties>
       </Project>
     </ItemGroup>
-    <ItemGroup>
-      <Project>
-        <PyMSBuild_Properties Condition="%(Project.TargetName) != ''">%(PyMSBuild_Properties);TargetName=%(Project.TargetName)</PyMSBuild_Properties>
-        <PyMSBuild_Properties Condition="%(Project.TargetExt) != ''">%(PyMSBuild_Properties);TargetExt=%(Project.TargetExt)</PyMSBuild_Properties>
-      </Project>
-    </ItemGroup>
     <ItemGroup>
       <Project>
         <Properties Condition="%(Properties) != ''">%(PyMSBuild_Properties);%(Properties)</Properties>
@@ -79,7 +73,7 @@
     <ItemGroup>
       <_DistFiles Remove="@(_DistFiles)" />
       <_DistFiles Include="@(PyMSBuild_LayoutFiles)" Condition="%(IncludeInLayout)">
-        <Destination>$([msbuild]::EnsureTrailingSlash($([msbuild]::EnsureTrailingSlash($(SourceDir)))%(TargetDir)))%(TargetName)%(TargetExt)</Destination>
+        <Destination>$([msbuild]::EnsureTrailingSlash($([msbuild]::EnsureTrailingSlash($(_SourceLayoutDir)))%(TargetDir)))%(TargetName)%(TargetExt)</Destination>
       </_DistFiles>
       <!-- Remove anything that is being copied directly from the source tree -->
       <_Removing Include="@(_DistFiles)" Condition="%(FullPath) == %(Destination)" />
diff --git a/pymsbuild/targets/pyd.props b/pymsbuild/targets/pyd.props
index f3d017e..b77ed41 100644
--- a/pymsbuild/targets/pyd.props
+++ b/pymsbuild/targets/pyd.props
@@ -1,6 +1,5 @@
 <Project>
   <PropertyGroup>
-    <TargetExt Condition="$(__TargetExt) != ''">$(__TargetExt)</TargetExt>
     <TargetPath>$([System.IO.Path]::Combine(`$(ProjectDir)`,`$(OutDir)`,`$(TargetName)$(TargetExt)`))</TargetPath>
     <LinkIncremental>false</LinkIncremental>
   </PropertyGroup>
@@ -13,6 +12,8 @@
     <Link>
       <AdditionalLibraryDirectories>$(PythonLibs);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
       <GenerateDebugInformation>True</GenerateDebugInformation>
+      <OutputFile>$(TargetPath)</OutputFile>
+      <ProgramDatabaseFile>$([System.IO.Path]::ChangeExtension('$(TargetPath)', '.pdb'))</ProgramDatabaseFile>
     </Link>
     <Lib>
       <OutputFile>$(TargetPath)</OutputFile>
diff --git a/pymsbuild/targets/pyd.targets b/pymsbuild/targets/pyd.targets
index 2fe755a..de6fbf6 100644
--- a/pymsbuild/targets/pyd.targets
+++ b/pymsbuild/targets/pyd.targets
@@ -11,7 +11,7 @@
       <_PydVersionInfo Include="@(PydVersionInfo)">
         <RC>$(IntDir)version.rc</RC>
       </_PydVersionInfo>
-      <ResourceCompile Include="%(PydVersionInfo.RC)">
+      <ResourceCompile Include="%(_PydVersionInfo.RC)">
         <IncludeInSdist>false</IncludeInSdist>
         <IncludeInLayout>false</IncludeInLayout>
         <IncludeInWheel>false</IncludeInWheel>
@@ -19,9 +19,7 @@
     </ItemGroup>
   </Target>
 
-  <Target Name="_WriteVersionInfo"
-          Inputs="@(_PydVersionInfo)"
-          Outputs="%(_PydVersionInfo.RC)">
+  <Target Name="_WriteVersionInfo" Inputs="@(_PydVersionInfo->'__nonexistent')" Outputs="%(_PydVersionInfo.RC)">
     <PropertyGroup>
       <_PydVersionInfoLines><![CDATA[
 #include <windows.h>
@@ -75,9 +73,11 @@ END]]></_PydVersionInfoLines>
     <WriteLinesToFile File="%(_PydVersionInfo.RC)"
                       Lines="$(_PydVersionInfoLines.Trim())"
                       Encoding="%(_PydVersionInfo.Encoding)"
-                      Overwrite="true"
-                      WriteOnlyWhenDifferent="true" />
+                      Overwrite="true" />
     <Message Text="Generated version info" />
+    <ItemGroup>
+      <FileWrites Include="%(_PydVersionInfo.RC)" />
+    </ItemGroup>
   </Target>
 
   <Target Name="_GenerateVersionInfo"
diff --git a/pymsbuild/targets/runtime-download.py b/pymsbuild/targets/runtime-download.py
index a5a01fe..8624487 100644
--- a/pymsbuild/targets/runtime-download.py
+++ b/pymsbuild/targets/runtime-download.py
@@ -6,6 +6,7 @@
 import re
 import sys
 import zipfile
+from fnmatch import fnmatch
 from urllib.request import urlopen
 
 DEFAULT_TAG = "cp{0}{1}-cp{0}{1}-{2}".format(
@@ -14,14 +15,22 @@
     {"": "win_amd64", "32": "win32", "-arm64": "win_arm64"}.get(sys.winver.partition("-")[2]),
 )
 
+DEFAULT_VERSIONLESS_STDLIB = "stdlib.zip"
+
 parser = argparse.ArgumentParser()
 parser.add_argument("-o", metavar="PATH", type=pathlib.Path, default=".", required=False, help="Output directory")
+parser.add_argument("--skip-download", action="store_true", help="Assume already downloaded and extracted")
 parser.add_argument("--nuget", action="store_true", help="Acquire from Nuget")
 parser.add_argument("--embed", action="store_true", help="Acquire embeddable from python.org")
 parser.add_argument("--tag", default=DEFAULT_TAG, required=False, help="Specific wheel tag")
 parser.add_argument("--platform", required=False, help="Override platform (win32, win_amd64, win_arm64)")
 parser.add_argument("--version", required=False, help="Override version")
 parser.add_argument("--dry-run", action="store_true", help="Do not write files to disk")
+parser.add_argument("--exclude", required=False, help=";/: separated path globs to omit")
+parser.add_argument("--versionless", action="store_true", help="Remove version numbers from runtime files")
+parser.add_argument("--versionless-stdlib-zip", metavar="FILENAME", default="stdlib.zip", type=str, help="Versionless name for stdlib ZIP")
+parser.add_argument("--versionless-runtime-dll", metavar="FILENAME", default="python.dll", type=str, help="Versionless name for runtime DLL")
+parser.add_argument("--versionless-runtime-so", metavar="FILENAME", default="libpython.so", type=str, help="Versionless name for runtime SO")
 args = parser.parse_args()
 
 m = re.match(r"(?:cp|py)(\d)(\d+)-.+?-(win.+)", args.tag)
@@ -116,25 +125,25 @@ def from_embed(version, platform):
     with urlopen(url) as r:
         versions = [s.decode() for s in re.findall(rb'<a\s+href="(\d+\.\d+\.\d+)', r.read())]
 
-    if version not in versions:
-        released = [v for v in versions if v.startswith(f"{version}.")]
-        if released:
-            version = max(released, key=ver_key)
-        else:
-            raise Exception(f"ERROR: No matching versions found for package {package}")
-
-    print("Selected", version)
-    url = f"{url}{version}/"
+    if version in versions:
+        released = [version]
+    else:
+        released = sorted((v for v in versions if v.startswith(f"{version}.")), reverse=True, key=ver_key)
 
     pattern = re.escape(package).replace("VERSION", "(.+?)")
     pattern = f'a href="({pattern})"'
-    with urlopen(url) as r:
-        files = [(i[1].decode(), i[0].decode()) for i in re.findall(pattern.encode(), r.read())]
+    for version in released:
+        with urlopen(f"{url}{version}/") as r:
+            files = [(i[1].decode(), i[0].decode()) for i in re.findall(pattern.encode(), r.read())]
+
+        files = sorted(files, key=lambda i: ver_key(i[0]), reverse=True)
+        if files:
+            url = f"{url}{version}/{files[0][1]}"
+            break
+    else:
+        raise Exception(f"ERROR: No matching versions found for package {package}")
 
-    files = sorted(files, key=lambda i: ver_key(i[0]), reverse=True)
-    if not files:
-        raise Exception(f"ERROR: No matching versions found for {version}")
-    url += files[0][1]
+    print("Selected", version)
     print("Downloading from", url)
     buffer = io.BytesIO()
     with urlopen(url) as r:
@@ -147,18 +156,44 @@ def from_embed(version, platform):
             yield n, zf.read(n)
 
 
+def from_dir(version, platform):
+    dest = args.o
+    return ((x.name, x.read_bytes()) for x in dest.rglob("*"))
+
+
 try:
-    func = from_nuget if args.nuget else from_embed if args.embed else None
+    func = from_dir if args.skip_download else from_nuget if args.nuget else from_embed if args.embed else None
     if not func:
         raise Exception("ERROR: No source specified (pass --nuget or --embed)")
+    args.o = args.o.absolute()
+    exclusions = [x for x in args.exclude.split(os.pathsep) if x] if args.exclude else ()
     for name, content in func(args.version, args.platform):
+        dest = args.o / name
         if args.dry_run:
             print("Write", name, len(content), "bytes")
         else:
-            dest = args.o / name
+            target = dest
+            if args.versionless:
+                if dest.match("python3*.zip"):
+                    target = dest.with_name(args.versionless_stdlib_zip)
+                """DISABLED: the effect of --versionless on the DLL or the SO.
+
+                elif dest.match("python3.dll"):
+                    continue
+                elif dest.match("python3*.dll"):
+                    target = dest.with_name(args.versionless_runtime_dll)
+                elif dest.match("python3*._pth"):
+                    target = dest.with_name(args.versionless_runtime_dll).with_suffix("._pth")
+                elif dest.match("libpython3.*.so"):
+                    target = dest.with_name(args.versionless_runtime_so)
+                elif dest.match("libpython3.*._pth"):
+                    target = dest.with_name(args.versionless_runtime_so).with_suffix("._pth")
+                """
             dest.parent.mkdir(parents=True, exist_ok=True)
             with open(dest, "wb") as f:
                 f.write(content)
+            if not any(dest.match(x) for x in exclusions):
+                print("FILE:{}{}{}".format(dest, os.pathsep, target.relative_to(args.o)))
 
 except Exception as ex:
     if type(ex) is not Exception:
diff --git a/pymsbuild/targets/runtime-gcc.targets b/pymsbuild/targets/runtime-gcc.targets
new file mode 100644
index 0000000..8f66464
--- /dev/null
+++ b/pymsbuild/targets/runtime-gcc.targets
@@ -0,0 +1,19 @@
+<Project>
+  <Target Name="_DownloadRuntime_Properties">
+    <PropertyGroup>
+      <PythonRuntimeDir Condition="$(PythonRuntimeDir) == ''">$(IntDir)python</PythonRuntimeDir>
+      <PythonRuntimeEmbeddable Condition="$(PythonRuntimeEmbeddable) != 'true'">false</PythonRuntimeEmbeddable>
+      <PythonRuntimeVersionless Condition="$(PythonRuntimeVersionless) != 'false'">true</PythonRuntimeVersionless>
+      <PythonRuntimeExclude Condition="$(PythonRuntimeExecutables) != 'true'">python*+x|$(PythonRuntimeExclude)</PythonRuntimeExclude>
+      <PythonRuntimeExclude Condition="$(Python_PthFile) == 'false'">*._pth|$(PythonRuntimeExclude)</PythonRuntimeExclude>
+    </PropertyGroup>
+    <ItemGroup>
+      <_ExistingPythonRuntime Include="$(PythonRuntimeDir)/**/*" />
+    </ItemGroup>
+  </Target>
+  <Target Name="_DownloadRuntime_Exec">
+    <Message Text="TODO: Get runtime for GCC-based platforms" Importance="high" />
+  </Target>
+  <Target Name="DownloadRuntime" DependsOnTargets="_DownloadRuntime_Properties;_DownloadRuntime_Exec">
+  </Target>
+</Project>
diff --git a/pymsbuild/targets/runtime-windows.targets b/pymsbuild/targets/runtime-windows.targets
new file mode 100644
index 0000000..e4fcaac
--- /dev/null
+++ b/pymsbuild/targets/runtime-windows.targets
@@ -0,0 +1,57 @@
+<Project>
+  <Target Name="_DownloadRuntime_Properties">
+    <PropertyGroup>
+      <PythonRuntimeDir Condition="$(PythonRuntimeDir) == ''">$(IntDir)python</PythonRuntimeDir>
+      <PythonRuntimeEmbeddable Condition="$(PythonRuntimeEmbeddable) != 'true'">false</PythonRuntimeEmbeddable>
+      <PythonRuntimeVersionless Condition="$(PythonRuntimeVersionless) != 'false'">true</PythonRuntimeVersionless>
+      <PythonRuntimeExclude Condition="$(PythonRuntimeExecutables) != 'true'">*.exe|$(PythonRuntimeExclude)</PythonRuntimeExclude>
+      <PythonRuntimeExclude Condition="$(Python_PthFile) == 'false'">*._pth|$(PythonRuntimeExclude)</PythonRuntimeExclude>
+    </PropertyGroup>
+    <ItemGroup>
+      <_ExistingPythonRuntime Include="$(PythonRuntimeDir)/**/*" />
+    </ItemGroup>
+  </Target>
+  <Target Name="_DownloadRuntime_Exec" Condition="$(HostPython) != ''">
+    <PropertyGroup>
+      <_Cmd>"$(HostPython)" "$(MSBuildThisFileDirectory)runtime-download.py"</_Cmd>
+      <_Cmd>$(_Cmd) -o "$(PythonRuntimeDir)"</_Cmd>
+      <_Cmd Condition="@(_ExistingPythonRuntime) != ''">$(_Cmd) --skip-download</_Cmd>
+      <_Cmd Condition="$(PythonRuntimeEmbeddable)">$(_Cmd) --embed</_Cmd>
+      <_Cmd Condition="!$(PythonRuntimeEmbeddable)">$(_Cmd) --nuget</_Cmd>
+      <_Cmd Condition="$(PythonRuntimeVersion) != ''">$(_Cmd) --version "$(PythonRuntimeVersion)"</_Cmd>
+      <_Cmd Condition="$(PythonRuntimePlatform) != ''">$(_Cmd) --platform "$(PythonRuntimePlatform)"</_Cmd>
+      <_Cmd Condition="$(PythonRuntimeTag) != ''">$(_Cmd) --tag "$(PythonRuntimeTag)"</_Cmd>
+      <_Cmd Condition="$(PythonRuntimeExclude) != ''">$(_Cmd) --exclude "$(PythonRuntimeExclude.Replace(`|`, `;`))"</_Cmd>
+      <_Cmd Condition="$(PythonRuntimeVersionless) != ''">$(_Cmd) --versionless</_Cmd>
+      <_Cmd Condition="$(PythonRuntimeVersionlessStdlibZip) != ''">$(_Cmd) --versionless-stdlib-zip "$(PythonRuntimeVersionlessStdlibZip)"</_Cmd>
+      <_Cmd Condition="$(PythonRuntimeVersionlessRuntimeDll) != ''">$(_Cmd) --versionless-runtime-dll "$(PythonRuntimeVersionlessRuntimeDll)"</_Cmd>
+      <_Cmd Condition="$(PythonRuntimeVersionlessRuntimeSO) != ''">$(_Cmd) --versionless-runtime-so "$(PythonRuntimeVersionlessRuntimeSO)"</_Cmd>
+    </PropertyGroup>
+    <Message Text="Download command: $(_Cmd)" Importance="$(_Low)" />
+    <Exec Command="$(_Cmd)" ConsoleToMsBuild="true" StandardOutputImportance="$(_Low)" StandardErrorImportance="High" UseUtf8Encoding="Always">
+      <Output TaskParameter="ConsoleOutput" ItemName="_DownloadOutput" />
+    </Exec>
+    <ItemGroup>
+      <FileWrites Include="$(PythonRuntimeDir)/**/*" />
+    </ItemGroup>
+  </Target>
+  <Target Name="_Fill_PythonRuntime" Inputs="@(_DownloadOutput)" Outputs="__bogus">
+    <ItemGroup>
+      <PythonRuntime Include="$([System.String]::Copy('%(_DownloadOutput.Identity)').Substring(5).Split('$(_PathSep)')[0])"
+                     Condition="$([System.String]::Copy('%(_DownloadOutput.Identity)').StartsWith('FILE:'))">
+        <_FullTarget>$([System.String]::Copy('%(_DownloadOutput.Identity)').Substring(5).Split('$(_PathSep)')[1])</_FullTarget>
+      </PythonRuntime>
+    </ItemGroup>
+  </Target>
+  <Target Name="DownloadRuntime"
+          DependsOnTargets="_DownloadRuntime_Properties;_DownloadRuntime_Exec;_Fill_PythonRuntime">
+    <ItemGroup>
+      <PythonRuntime>
+        <TargetDir>$([System.IO.Path]::GetDirectoryName('%(_FullTarget)'))</TargetDir>
+        <TargetName>$([System.IO.Path]::GetFileNameWithoutExtension('%(_FullTarget)'))</TargetName>
+        <TargetExt>$([System.IO.Path]::GetExtension('%(_FullTarget)'))</TargetExt>
+      </PythonRuntime>
+    </ItemGroup>
+    <Message Text="Python Runtime files:%0A@(PythonRuntime->'- %(Identity) (%(TargetName))', '%0A')" Importance="high" Condition="$(ShowRuntimeFiles) == 'true'" />
+  </Target>
+</Project>
diff --git a/pymsbuild/targets/runtime.targets b/pymsbuild/targets/runtime.targets
index 9ad71a1..6f045e3 100644
--- a/pymsbuild/targets/runtime.targets
+++ b/pymsbuild/targets/runtime.targets
@@ -1,41 +1,4 @@
 <Project>
-  <Target Name="_DownloadRuntime_Properties">
-    <PropertyGroup>
-      <PythonRuntimeDir Condition="$(PythonRuntimeDir) == ''">$(IntDir)python</PythonRuntimeDir>
-      <PythonRuntimeExecutables Condition="$(PythonRuntimeExecutables) != 'true'">false</PythonRuntimeExecutables>
-      <PythonRuntimeEmbeddable Condition="$(PythonRuntimeEmbeddable) != 'true'">false</PythonRuntimeEmbeddable>
-    </PropertyGroup>
-    <ItemGroup>
-      <_ExistingPythonRuntime Include="$(PythonRuntimeDir)/**/*" />
-    </ItemGroup>
-  </Target>
-  <Target Name="_DownloadRuntime_Exec" Condition="@(_ExistingPythonRuntime) == ''">
-    <PropertyGroup>
-      <_Cmd>"$(HostPython)" "$(MSBuildThisFileDirectory)runtime-download.py"</_Cmd>
-      <_Cmd>$(_Cmd) -o "$(PythonRuntimeDir)"</_Cmd>
-      <_Cmd Condition="$(PythonRuntimeEmbeddable)">$(_Cmd) --embed</_Cmd>
-      <_Cmd Condition="!$(PythonRuntimeEmbeddable)">$(_Cmd) --nuget</_Cmd>
-      <_Cmd Condition="$(PythonRuntimeVersion) != ''">$(_Cmd) --version $(PythonRuntimeVersion)</_Cmd>
-      <_Cmd Condition="$(PythonRuntimePlatform) != ''">$(_Cmd) --platform $(PythonRuntimePlatform)</_Cmd>
-      <_Cmd Condition="$(PythonRuntimeTag) != ''">$(_Cmd) --tag $(PythonRuntimeTag)</_Cmd>
-    </PropertyGroup>
-    <Exec Command="$(_Cmd)" />
-    <ItemGroup>
-      <FileWrites Include="$(PythonRuntimeDir)/**/*" />
-    </ItemGroup>
-  </Target>
-  <Target Name="DownloadRuntime" DependsOnTargets="_DownloadRuntime_Properties;_DownloadRuntime_Exec">
-    <ItemGroup>
-      <PythonRuntime Include="$(PythonRuntimeDir)/**/*" Exclude="$(PythonRuntimeDir)/*.exe">
-        <TargetDir>%(RecursiveDir)</TargetDir>
-        <TargetName>%(Filename)</TargetName>
-        <TargetExt>%(Extension)</TargetExt>
-      </PythonRuntime>
-      <PythonRuntime Include="$(PythonRuntimeDir)/*.exe" Condition="$(PythonRuntimeExecutables)">
-        <TargetDir>%(RecursiveDir)</TargetDir>
-        <TargetName>%(Filename)</TargetName>
-        <TargetExt>%(Extension)</TargetExt>
-      </PythonRuntime>
-    </ItemGroup>
-  </Target>
+  <Import Project="runtime-windows.targets" Condition="$(PlatformToolset) != 'gcc'" />
+  <Import Project="runtime-gcc.targets" Condition="$(PlatformToolset) == 'gcc'" />
 </Project>
diff --git a/pymsbuild/vendor.py b/pymsbuild/vendor.py
new file mode 100644
index 0000000..7159ab7
--- /dev/null
+++ b/pymsbuild/vendor.py
@@ -0,0 +1,132 @@
+import os
+import re
+import sys
+
+from pathlib import PurePath, Path
+from pymsbuild import get_current_build_state
+from pymsbuild._types import *
+from pymsbuild.dllpack import DllPackage, PydRedirect
+
+__all__ = ["VendoredPackage", "VendoredDllPackage"]
+
+def _norm(name):
+    return re.sub(r"[^a-z0-9]", "_", name.lower())
+
+
+class _VendoredMixin:
+    __ALL = "$ALL_VENDORED"
+    __FIND_ALL = "**/" + __ALL
+    __site_cache = {}
+
+    @classmethod
+    def _name_from_spec(cls, spec):
+        m = re.match(r"([A-Za-z0-9\-_.]+)\s*(.*)$", spec)
+        if not m:
+            raise ValueError(f"Unsupported package spec: {spec}")
+        return _norm(m.groups()[0])
+
+    @classmethod
+    def collect_all_requirements(cls, build_wheel_requires, package):
+        all_seen = set()
+        for s in build_wheel_requires:
+            all_seen.add(cls._name_from_spec(s))
+        for m in package.findall(cls.__FIND_ALL):
+            if cls._name_from_spec(m.spec) not in all_seen:
+                build_wheel_requires.append(m.spec)
+                all_seen.add(cls._name_from_spec(m.spec))
+
+    @classmethod
+    def collect_all(cls, package, tag):
+        for m in package.findall(cls.__FIND_ALL):
+            m.collect(tag)
+
+    def _match(self, key):
+        if key == self.__ALL:
+            return True
+        return PurePath(self.name).match(key)
+
+    def collect(self, tag):
+        found = 0
+        site = self.__site_cache
+        if not site:
+            site.update({
+                _norm(di.name.partition("-")[0]): di
+                for p in sys.path
+                for di in Path(p).glob("*.dist-info")
+                if di.is_dir()
+            })
+        try:
+            p = site[self._name_from_spec(self.spec)]
+        except KeyError:
+            raise LookupError(f"Package {self.spec} is not installed") from None
+        source = p.parent
+        with open(p / "RECORD", "r", encoding="utf-8") as f:
+            for line in f:
+                if not line.strip() or line.lstrip().startswith("#"):
+                    continue
+                filename, filehash, filesize = line.rsplit(",", 2)
+                if filename.startswith(".."):
+                    continue
+                if self._add_vendor_member(source / filename, filename, filehash, filesize):
+                    found += 1
+        if get_current_build_state().verbose and not get_current_build_state().quiet:
+            print(f"Vendoring {found} file{'s' if found != 1 else ''} for {self.name}")
+
+
+class VendoredPackage(Package, _VendoredMixin):
+    def __init__(self, spec, name=None, **options):
+        name = name or self._name_from_spec(spec)
+        options.setdefault("BuildWheelRequires", spec)
+        super().__init__(name, **options)
+        self.spec = spec
+
+    def _add_vendor_member(self, source, name, filehash, filesize):
+        if not self.options.get("as_search_path"):
+            package, _, name = name.partition("/")
+            if package != self.name:
+                if package == f"{self.name}.py" and not name:
+                    # Convert single-file packages to modules
+                    name = "__init__.py"
+                    package = None
+                else:
+                    return
+
+        suffix = source.suffix.lower()
+        if suffix == ".pyc":
+            return
+        kind = File
+        if suffix == ".py":
+            kind = PyFile
+        self.members.append(kind(source, name, IncludeInSdist=False))
+        return True
+
+
+class VendoredDllPackage(DllPackage, _VendoredMixin):
+    def __init__(self, spec, name=None, import_names=[], **options):
+        name = name or self._name_from_spec(spec)
+        options.setdefault("BuildWheelRequires", spec)
+        super().__init__(name, **options)
+        self.spec = spec
+        if self.options.get("as_search_path"):
+            raise ValueError("Cannot set 'as_search_path' on VendoredDllPackage")
+
+    def _add_vendor_member(self, source, name, filehash, filesize):
+        package, _, name = name.partition("/")
+        if package != self.name:
+            if package == f"{self.name}.py" and not name:
+                # Convert single-file packages to modules
+                name = "__init__.py"
+                package = None
+            else:
+                return
+
+        suffix = source.suffix.lower()
+        if suffix == ".pyc":
+            return
+        kind = File
+        if suffix in (".pyd", ".so", ".dylib"):
+            kind = PydRedirect
+        elif suffix == ".py":
+            kind = PyFile
+        self.members.append(kind(source, name, IncludeInSdist=False))
+        return True
diff --git a/samples/azure-cli/README.md b/samples/azure-cli/README.md
new file mode 100644
index 0000000..02c7608
--- /dev/null
+++ b/samples/azure-cli/README.md
@@ -0,0 +1,6 @@
+# azure-cli
+
+This sample project uses DLL packing, wheel dependencies and entrypoint
+to create a single package containing the entire Azure CLI tool.
+
+*Want more details? Ask on the issue tracker.*
diff --git a/samples/azure-cli/_msbuild.py b/samples/azure-cli/_msbuild.py
new file mode 100644
index 0000000..595fdd2
--- /dev/null
+++ b/samples/azure-cli/_msbuild.py
@@ -0,0 +1,137 @@
+import os
+import re
+import sys
+from importlib.util import find_spec
+from pathlib import Path, PurePath
+from pymsbuild import *
+from pymsbuild.dllpack import DllPackage, PydRedirect
+from pymsbuild.entrypoint import *
+from pymsbuild.vendor import *
+
+METADATA = {
+    "Metadata-Version": "2.1",
+    "Name": "azure-cli",
+    "Version": "1.0",
+    "Author": "Steve Dower",
+    "Author-email": "steve.dower@microsoft.com",
+    "Home-page": "https://github.com/zooba/pymsbuild",
+    "Project-url": [
+        "Bug Tracker, https://github.com/zooba/pymsbuild/issues",
+    ],
+    "Summary": "A sample of packing the Azure CLI",
+}
+
+
+with open(f"requirements-{sys.platform}.txt", "r", encoding="utf-8") as f:
+    METADATA["BuildWheelRequires"] = list(map(str.strip, f))
+
+
+def spec_name(spec):
+    return re.sub(r'[^a-z0-9]', '_', re.match(r'([a-z0-9.\-_]+)', spec.lower()).groups()[0])
+
+
+PACKAGE_SPECS = {spec_name(s): s for s in METADATA["BuildWheelRequires"] if not s.startswith("azure-")}
+
+# Packages where the package name doesn't match the import name.
+NAME_FIXUPS = {
+    "antlr4_python3_runtime": "antlr4",
+    "pygithub": "github",
+    "pyjwt": "jwt",
+    "pynacl": "nacl",
+    "pyopenssl": "OpenSSL",
+    "pysocks": "socks",
+    "python_dateutil": "dateutil",
+    "pyyaml": "yaml",
+    "websocket_client": "websocket",
+}
+
+for n in NAME_FIXUPS:
+    assert n in PACKAGE_SPECS, n
+
+PACKAGES = {
+    **{n: VendoredDllPackage(s, NAME_FIXUPS.get(n)) for n, s in PACKAGE_SPECS.items()},
+
+    # Override some packages that won't work as DLLs
+    "setuptools": VendoredPackage(PACKAGE_SPECS["setuptools"]),
+    "_distutils_hack": VendoredPackage(PACKAGE_SPECS["setuptools"], "_distutils_hack"),
+    "pkg_resources": VendoredPackage(PACKAGE_SPECS["setuptools"], "pkg_resources"),
+    "_cffi_backend": File("_cffi_backend", IncludeInSdist=False),
+}
+
+
+if "pywin32" in PACKAGES:
+    # pywin32 is so messy we just put it all in its own search path
+    PACKAGES["pywin32"] = VendoredPackage(PACKAGE_SPECS["pywin32"], as_search_path=True)
+    # We won't get pywin32.pth automatically, so add its paths instead
+    # We also add its 'import pywin32_bootstrap' to our main.py
+    PYWIN32_SEARCH_PATHS = [
+        SearchPath("vendored/pywin32"),
+        SearchPath("vendored/pywin32/pythonwin"),
+        SearchPath("vendored/pywin32/win32"),
+        SearchPath("vendored/pywin32/win32/lib"),
+    ]
+else:
+    PYWIN32_SEARCH_PATHS = []
+
+
+class SiteFile(File):
+    def _match(self, key):
+        if key == "$SITEFILE":
+            return True
+        return PurePath(self.name).match(key)
+
+
+PACKAGE = Package(
+    "azure-cli",
+    SourceFile("requirements-win32.txt"),
+    SourceFile("requirements-linux.txt"),
+
+    Package("azure"),
+
+    # Put all vendored packages in their own directory, and add a search path below
+    Package("vendored", *[p for p in PACKAGES.values() if p]),
+
+    PyFile("main.py"),
+    Entrypoint(
+        "az", "main", "run",
+        VersionInfo(
+            LegalCopyright="Copyright Microsoft",
+        ),
+        #Icon("Globe1.ico"),
+        SearchPath("."),
+        SearchPath("vendored"),
+        DefaultSearchPath(),
+        *PYWIN32_SEARCH_PATHS
+    ),
+)
+
+
+def init_METADATA():
+    import os, re
+    _, sep, version = os.getenv("GITHUB_REF", "").rpartition("/")
+    if sep and re.match(r"(\d+!)?\d+(\.\d+)+((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?$", version):
+        # Looks like a version tag
+        METADATA["Version"] = version
+
+
+def init_PACKAGE(tag=None):
+    if not tag:
+        return
+
+    VendoredPackage.collect_all(PACKAGE, tag)
+
+    spec = find_spec("_cffi_backend")
+    PACKAGES["_cffi_backend"].source = spec.origin
+    PACKAGES["_cffi_backend"].name = Path(spec.origin).name
+
+    azure = PACKAGE.find("azure")
+    import azure as azure_module
+    azure_paths = [Path(p) / n for p in azure_module.__path__ for n in os.listdir(p)]
+    for p in azure_paths:
+        azure.members.append(
+            DllPackage(
+                f"azure.{p.name}",
+                PyFile(p / "**/*.py"),
+                TargetName=p.name,
+            )
+        )
diff --git a/samples/azure-cli/main.py b/samples/azure-cli/main.py
new file mode 100644
index 0000000..2d46a59
--- /dev/null
+++ b/samples/azure-cli/main.py
@@ -0,0 +1,86 @@
+import setuptools
+try:
+    import pywin32_bootstrap
+except ModuleNotFoundError:
+    pass
+
+import azure.cli.core.aaz._command as aaz_command
+import azure.cli.core._help_loaders as help_loaders
+class HelpLoaderV2(help_loaders.BaseHelpLoader):
+    @property
+    def version(self):
+        return 2
+
+    def versioned_load(self, help_obj, parser):
+        pass
+
+    def get_noun_help_file_names(self, nouns):
+        pass
+
+    def load_entry_data(self, help_obj, parser):
+        pass
+
+    def load_help_body(self, help_obj):
+        pass
+
+    def load_help_parameters(self, help_obj):
+        pass
+
+    def load_help_examples(self, help_obj):
+        pass
+
+help_loaders.HelpLoaderV1 = HelpLoaderV2
+
+def _load_aaz_pkg(loader, pkg, parent_command_table, command_group_table, arg_str, fully_load):
+    """Override _load_aaz_pkg to use pkgutil to load rather than searching the disk
+    """
+    cut = False  # if cut, its sub commands and sub pkgs will not be added
+    command_table = {}  # the command available for this pkg and its sub pkgs
+    for value in pkg.__dict__.values():
+        if not fully_load and cut and command_table:
+            # when cut and command_table is not empty, stop loading more commands.
+            # the command_table should not be empty.
+            # Because if it's empty, the command group will be ignored in help if parent command group.
+            break
+        if isinstance(value, type):
+            if issubclass(value, aaz_command.AAZCommandGroup):
+                if value.AZ_NAME:
+                    # AAZCommandGroup already be registered by register_command_command
+                    if not arg_str.startswith(f'{value.AZ_NAME.lower()} '):
+                        # when args not contain command group prefix, then cut more loading.
+                        cut = True
+                    # add command group into command group table
+                    command_group_table[value.AZ_NAME] = value(
+                        cli_ctx=loader.cli_ctx)  # add command group even it's cut
+            elif issubclass(value, aaz_command.AAZCommand):
+                if value.AZ_NAME:
+                    # AAZCommand already be registered by register_command
+                    command_table[value.AZ_NAME] = value(loader=loader)
+
+    # continue load sub pkgs
+    import pkgutil
+    for mod in pkgutil.iter_modules(pkg.__path__):
+        sub_path = mod.name
+        if not fully_load and cut and command_table:
+            # when cut and command_table is not empty, stop loading more sub pkgs.
+            break
+        if sub_path.startswith('_'):
+            continue
+        try:
+            sub_pkg = aaz_command.importlib.import_module(f'.{sub_path}', pkg.__name__)
+        except ModuleNotFoundError:
+            aaz_command.logger.debug('Failed to load package folder in aaz: %s.',
+                                     aaz_command.os.path.join(pkg_path, sub_path))
+            continue
+
+        # recursively load sub package
+        _load_aaz_pkg(loader, sub_pkg, command_table, command_group_table, arg_str, fully_load)
+
+    parent_command_table.update(command_table)  # update the parent pkg's command table.
+
+
+aaz_command._load_aaz_pkg = _load_aaz_pkg
+
+
+def run():
+    import azure.cli.__main__
diff --git a/samples/azure-cli/pyproject.toml b/samples/azure-cli/pyproject.toml
new file mode 100644
index 0000000..ec53445
--- /dev/null
+++ b/samples/azure-cli/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ['pymsbuild>=1.0']
+build-backend = "pymsbuild"
\ No newline at end of file
diff --git a/samples/azure-cli/requirements-linux.txt b/samples/azure-cli/requirements-linux.txt
new file mode 100644
index 0000000..f046839
--- /dev/null
+++ b/samples/azure-cli/requirements-linux.txt
@@ -0,0 +1,140 @@
+adal==1.2.7
+antlr4-python3-runtime==4.13.1
+applicationinsights==0.11.10
+argcomplete==3.3.0
+azure-appconfiguration==1.1.1
+azure-batch==14.2.0
+azure-cli==2.62.0
+azure-cli-core==2.62.0
+azure-cli-telemetry==1.1.0
+azure-common==1.1.28
+azure-core==1.30.2
+azure-cosmos==3.2.0
+azure-data-tables==12.4.0
+azure-datalake-store==0.0.53
+azure-graphrbac==0.60.0
+azure-keyvault-administration==4.4.0b2
+azure-keyvault-certificates==4.7.0
+azure-keyvault-keys==4.9.0b3
+azure-keyvault-secrets==4.7.0
+azure-mgmt-advisor==9.0.0
+azure-mgmt-apimanagement==4.0.0
+azure-mgmt-appconfiguration==3.0.0
+azure-mgmt-appcontainers==2.0.0
+azure-mgmt-applicationinsights==1.0.0
+azure-mgmt-authorization==4.0.0
+azure-mgmt-batch==17.3.0
+azure-mgmt-batchai==7.0.0b1
+azure-mgmt-billing==6.0.0
+azure-mgmt-botservice==2.0.0
+azure-mgmt-cdn==12.0.0
+azure-mgmt-cognitiveservices==13.5.0
+azure-mgmt-compute==31.0.0
+azure-mgmt-containerinstance==10.1.0
+azure-mgmt-containerregistry==10.3.0
+azure-mgmt-containerservice==30.0.0
+azure-mgmt-core==1.4.0
+azure-mgmt-cosmosdb==9.5.1
+azure-mgmt-databoxedge==1.0.0
+azure-mgmt-datamigration==10.0.0
+azure-mgmt-devtestlabs==4.0.0
+azure-mgmt-dns==8.0.0
+azure-mgmt-eventgrid==10.2.0b2
+azure-mgmt-eventhub==10.1.0
+azure-mgmt-extendedlocation==1.0.0b2
+azure-mgmt-hdinsight==9.0.0
+azure-mgmt-imagebuilder==1.3.0
+azure-mgmt-iotcentral==10.0.0b2
+azure-mgmt-iothub==3.0.0
+azure-mgmt-iothubprovisioningservices==1.1.0
+azure-mgmt-keyvault==10.3.0
+azure-mgmt-kusto==0.3.0
+azure-mgmt-loganalytics==13.0.0b4
+azure-mgmt-managedservices==1.0.0
+azure-mgmt-managementgroups==1.0.0
+azure-mgmt-maps==2.0.0
+azure-mgmt-marketplaceordering==1.1.0
+azure-mgmt-media==9.0.0
+azure-mgmt-monitor==5.0.1
+azure-mgmt-msi==7.0.0
+azure-mgmt-netapp==10.1.0
+azure-mgmt-policyinsights==1.1.0b4
+azure-mgmt-privatedns==1.0.0
+azure-mgmt-rdbms==10.2.0b17
+azure-mgmt-recoveryservices==3.0.0
+azure-mgmt-recoveryservicesbackup==9.1.0
+azure-mgmt-redhatopenshift==1.4.0
+azure-mgmt-redis==14.3.0
+azure-mgmt-resource==23.1.1
+azure-mgmt-search==9.1.0
+azure-mgmt-security==6.0.0
+azure-mgmt-servicebus==8.2.0
+azure-mgmt-servicefabric==2.1.0
+azure-mgmt-servicefabricmanagedclusters==2.0.0b6
+azure-mgmt-servicelinker==1.2.0b2
+azure-mgmt-signalr==2.0.0b1
+azure-mgmt-sql==4.0.0b17
+azure-mgmt-sqlvirtualmachine==1.0.0b5
+azure-mgmt-storage==21.2.0
+azure-mgmt-synapse==2.1.0b5
+azure-mgmt-trafficmanager==1.0.0
+azure-mgmt-web==7.2.0
+azure-monitor-query==1.2.0
+azure-multiapi-storage==1.2.0
+azure-storage-common==1.4.2
+azure-synapse-accesscontrol==0.5.0
+azure-synapse-artifacts==0.19.0
+azure-synapse-managedprivateendpoints==0.4.0
+azure-synapse-spark==0.2.0
+bcrypt==4.1.3
+certifi==2024.7.4
+cffi==1.16.0
+chardet==5.2.0
+charset-normalizer==3.3.2
+colorama==0.4.6
+cryptography==42.0.8
+decorator==5.1.1
+Deprecated==1.2.14
+distro==1.9.0
+fabric==3.2.2
+humanfriendly==10.0
+idna==3.7
+invoke==2.2.0
+isodate==0.6.1
+javaproperties==0.5.2
+jmespath==1.0.1
+jsondiff==2.0.0
+knack==0.11.0
+msal==1.28.1
+msal-extensions==1.2.0b1
+msrest==0.7.1
+msrestazure==0.6.4.post1
+oauthlib==3.2.2
+packaging==24.1
+paramiko==3.4.0
+pkginfo==1.11.1
+portalocker==2.10.1
+psutil==5.9.8
+pycomposefile==0.0.31
+pycparser==2.22
+PyGithub==1.59.1
+Pygments==2.18.0
+PyJWT==2.8.0
+PyNaCl==1.5.0
+pyOpenSSL==24.1.0
+PySocks==1.7.1
+python-dateutil==2.9.0.post0
+PyYAML==6.0.1
+requests==2.32.3
+requests-oauthlib==2.0.0
+scp==0.13.6
+semver==2.13.0
+setuptools==70.2.0
+six==1.16.0
+sshtunnel==0.1.5
+tabulate==0.9.0
+typing_extensions==4.12.2
+urllib3==2.2.2
+websocket-client==1.3.3
+wrapt==1.16.0
+xmltodict==0.13.0
diff --git a/samples/azure-cli/requirements-win32.txt b/samples/azure-cli/requirements-win32.txt
new file mode 100644
index 0000000..5a6bc14
--- /dev/null
+++ b/samples/azure-cli/requirements-win32.txt
@@ -0,0 +1,146 @@
+adal==1.2.7
+antlr4-python3-runtime==4.13.1
+applicationinsights==0.11.10
+argcomplete==3.3.0
+azure-appconfiguration==1.1.1
+azure-batch==14.2.0
+azure-cli==2.61.0
+azure-cli-core==2.61.0
+azure-cli-telemetry==1.1.0
+azure-common==1.1.28
+azure-core==1.30.2
+azure-cosmos==3.2.0
+azure-data-tables==12.4.0
+azure-datalake-store==0.0.53
+azure-graphrbac==0.60.0
+azure-keyvault-administration==4.4.0b2
+azure-keyvault-certificates==4.7.0
+azure-keyvault-keys==4.9.0b3
+azure-keyvault-secrets==4.7.0
+azure-mgmt-advisor==9.0.0
+azure-mgmt-apimanagement==4.0.0
+azure-mgmt-appconfiguration==3.0.0
+azure-mgmt-appcontainers==2.0.0
+azure-mgmt-applicationinsights==1.0.0
+azure-mgmt-authorization==4.0.0
+azure-mgmt-batch==17.3.0
+azure-mgmt-batchai==7.0.0b1
+azure-mgmt-billing==6.0.0
+azure-mgmt-botservice==2.0.0
+azure-mgmt-cdn==12.0.0
+azure-mgmt-cognitiveservices==13.5.0
+azure-mgmt-compute==31.0.0
+azure-mgmt-containerinstance==10.1.0
+azure-mgmt-containerregistry==10.3.0
+azure-mgmt-containerservice==30.0.0
+azure-mgmt-core==1.4.0
+azure-mgmt-cosmosdb==9.4.0
+azure-mgmt-databoxedge==1.0.0
+azure-mgmt-datalake-nspkg==3.0.1
+azure-mgmt-datalake-store==0.5.0
+azure-mgmt-datamigration==10.0.0
+azure-mgmt-devtestlabs==4.0.0
+azure-mgmt-dns==8.0.0
+azure-mgmt-eventgrid==10.2.0b2
+azure-mgmt-eventhub==10.1.0
+azure-mgmt-extendedlocation==1.0.0b2
+azure-mgmt-hdinsight==9.0.0
+azure-mgmt-imagebuilder==1.3.0
+azure-mgmt-iotcentral==10.0.0b2
+azure-mgmt-iothub==3.0.0
+azure-mgmt-iothubprovisioningservices==1.1.0
+azure-mgmt-keyvault==10.3.0
+azure-mgmt-kusto==0.3.0
+azure-mgmt-loganalytics==13.0.0b4
+azure-mgmt-managedservices==1.0.0
+azure-mgmt-managementgroups==1.0.0
+azure-mgmt-maps==2.0.0
+azure-mgmt-marketplaceordering==1.1.0
+azure-mgmt-media==9.0.0
+azure-mgmt-monitor==5.0.1
+azure-mgmt-msi==7.0.0
+azure-mgmt-netapp==10.1.0
+azure-mgmt-nspkg==3.0.2
+azure-mgmt-policyinsights==1.1.0b4
+azure-mgmt-privatedns==1.0.0
+azure-mgmt-rdbms==10.2.0b17
+azure-mgmt-recoveryservices==3.0.0
+azure-mgmt-recoveryservicesbackup==9.1.0
+azure-mgmt-redhatopenshift==1.4.0
+azure-mgmt-redis==14.3.0
+azure-mgmt-resource==23.1.1
+azure-mgmt-search==9.1.0
+azure-mgmt-security==6.0.0
+azure-mgmt-servicebus==8.2.0
+azure-mgmt-servicefabric==2.1.0
+azure-mgmt-servicefabricmanagedclusters==2.0.0b6
+azure-mgmt-servicelinker==1.2.0b2
+azure-mgmt-signalr==2.0.0b1
+azure-mgmt-sql==4.0.0b16
+azure-mgmt-sqlvirtualmachine==1.0.0b5
+azure-mgmt-storage==21.1.0
+azure-mgmt-synapse==2.1.0b5
+azure-mgmt-trafficmanager==1.0.0
+azure-mgmt-web==7.2.0
+azure-monitor-query==1.2.0
+azure-multiapi-storage==1.2.0
+azure-nspkg==3.0.2
+azure-storage-common==1.4.2
+azure-synapse-accesscontrol==0.5.0
+azure-synapse-artifacts==0.18.0
+azure-synapse-managedprivateendpoints==0.4.0
+azure-synapse-spark==0.2.0
+bcrypt==4.1.3
+certifi==2024.7.4
+cffi==1.16.0
+chardet==5.2.0
+charset-normalizer==3.3.2
+colorama==0.4.6
+cryptography==42.0.8
+decorator==5.1.1
+Deprecated==1.2.14
+fabric==3.2.2
+humanfriendly==10.0
+idna==3.7
+invoke==2.2.0
+isodate==0.6.1
+javaproperties==0.5.2
+jmespath==1.0.1
+jsondiff==2.0.0
+knack==0.11.0
+msal==1.28.0
+msal-extensions==1.2.0b1
+msrest==0.7.1
+msrestazure==0.6.4.post1
+oauthlib==3.2.2
+packaging==24.1
+paramiko==3.4.0
+pkginfo==1.11.1
+portalocker==2.10.0
+psutil==5.9.8
+pycomposefile==0.0.31
+pycparser==2.22
+PyGithub==1.59.1
+Pygments==2.18.0
+PyJWT==2.8.0
+pymsalruntime==0.14.2
+PyNaCl==1.5.0
+pyOpenSSL==24.1.0
+pyreadline3==3.4.1
+PySocks==1.7.1
+python-dateutil==2.9.0.post0
+pywin32==306
+PyYAML==6.0.1
+requests==2.32.3
+requests-oauthlib==2.0.0
+scp==0.13.6
+semver==2.13.0
+setuptools==70.2.0
+six==1.16.0
+sshtunnel==0.1.5
+tabulate==0.9.0
+typing_extensions==4.12.2
+urllib3==2.2.2
+websocket-client==1.3.3
+wrapt==1.16.0
+xmltodict==0.13.0
diff --git a/samples/azure-pack/_msbuild.py b/samples/azure-pack/_msbuild.py
index 9f5ba87..2718ac4 100644
--- a/samples/azure-pack/_msbuild.py
+++ b/samples/azure-pack/_msbuild.py
@@ -45,7 +45,7 @@
 
 
 PACKAGE = Package("azure-pack",
-    SourceFile("requirements.txt"),
+    SourceFile("requirements-win32.txt"),
     File("azure-pack.pth.in", name="../azure-pack.pth", IncludeInLayout=False),
     *PACKAGES.values(),
 )
@@ -58,7 +58,7 @@ def init_METADATA():
         # Looks like a version tag
         METADATA["Version"] = version
 
-    with open("requirements.txt", "r", encoding="utf-8") as f:
+    with open("requirements-win32.txt", "r", encoding="utf-8") as f:
         METADATA["BuildWheelRequires"] = list(map(str.strip, f))
 
 
diff --git a/samples/azure-pack/requirements.txt b/samples/azure-pack/requirements-win32.txt
similarity index 100%
rename from samples/azure-pack/requirements.txt
rename to samples/azure-pack/requirements-win32.txt


Test patch:
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/test.yml
similarity index 87%
rename from .github/workflows/pythonpackage.yml
rename to .github/workflows/test.yml
index b1e3c08..d35a7f4 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/test.yml
@@ -29,7 +29,7 @@ jobs:
     - name: Install dependencies
       run: pip install -r tests/requirements.txt
     - name: Test with pytest
-      run: pytest
+      run: pytest --ignore tests/test_samples.py
       env:
         PYTHON_CONFIG: python3-config
     - name: Check self build (in-place)
@@ -44,3 +44,7 @@ jobs:
       run: python -m pymsbuild wheel
       env:
         PYTHON_CONFIG: python3-config
+    - name: Check samples
+      run: pytest tests/test_samples.py
+      env:
+        PYTHON_CONFIG: python3-config
diff --git a/pytest.ini b/pytest.ini
index 5c023ce..896d00d 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -3,4 +3,4 @@ python_classes = Test*
 python_files = test_*.py
 python_functions = test_*
 testpaths = tests
-addopts = -v
+addopts = -vv
diff --git a/samples/azure-cli/tests/test-sample.py b/samples/azure-cli/tests/test-sample.py
new file mode 100644
index 0000000..8199ee3
--- /dev/null
+++ b/samples/azure-cli/tests/test-sample.py
@@ -0,0 +1,11 @@
+import os
+import pathlib
+import subprocess
+
+AZ_EXE = "az.exe" if os.name == "nt" else "az"
+
+AZ = pathlib.Path(os.getenv("BUILD_PREFIX")) / "azure-cli" / AZ_EXE
+
+assert AZ.is_file()
+
+subprocess.check_call([AZ, "--help", "--debug"])
diff --git a/tests/conftest.py b/tests/conftest.py
index 0aac7bf..c9cfa3b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,11 +1,15 @@
+import os
 import pytest
 
 from pathlib import Path
 
 @pytest.fixture(scope="session")
 def testdata():
-    return Path(__file__).absolute().parent / "testdata"
+    try:
+        return Path(os.environ["PYMSBUILD_TEST_TESTDATA"])
+    except KeyError:
+        return Path(__file__).absolute().parent
 
 @pytest.fixture(scope="session")
-def inittestprojects():
-    return Path(__file__).absolute().parent / "testinit"
+def inittestprojects(testdata):
+    return testdata / "testinit"
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 737f27a..e65e4d5 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -3,3 +3,4 @@ cryptography
 Cython
 packaging
 pytest
+simpleindex
diff --git a/tests/simpleindex.toml.in b/tests/simpleindex.toml.in
new file mode 100644
index 0000000..1ada9c2
--- /dev/null
+++ b/tests/simpleindex.toml.in
@@ -0,0 +1,11 @@
+[routes."pymsbuild"]
+source = "path"
+to = "."
+
+[routes."{project}"]
+source = "http"
+to = "https://pypi.org/simple/{project}/"
+
+[server]
+host = "127.0.0.1"
+port = 65432
diff --git a/tests/test_build.py b/tests/test_build.py
index 98ee500..776005a 100644
--- a/tests/test_build.py
+++ b/tests/test_build.py
@@ -25,16 +25,16 @@
 def build_state(tmp_path, testdata):
     bs = BuildState()
     bs.verbose = True
-    bs.source_dir = testdata
+    bs.source_dir = testdata / "testdata"
     bs.output_dir = tmp_path / "out"
     bs.build_dir = tmp_path / "build"
     bs.layout_dir = tmp_path / "layout"
     bs._perform_layout = False  # allows us to override layout_dir
     bs.temp_dir = tmp_path / "temp"
     bs.package = T.Package("package",
-        T.PyFile(testdata / "empty.py", "__init__.py"),
+        T.PyFile(testdata / "testdata/empty.py", "__init__.py"),
         T.PydFile("mod",
-            T.CSourceFile(testdata / "mod.c"),
+            T.CSourceFile(testdata / "testdata/mod.c"),
             T.VersionInfo(
                 ProductName="package",
                 FileDescription="The package module",
@@ -165,9 +165,9 @@ def test_build_wheel_layout(build_state):
 
 @pytest.mark.parametrize("proj", ["testcython", "testproject1", "testpurepy"])
 @pytest.mark.parametrize("configuration", ["Debug", "Release"])
-def test_build_test_project(build_state, proj, configuration):
+def test_build_test_project(build_state, testdata, proj, configuration):
     bs = build_state
-    bs.source_dir = bs.source_dir.parent / proj
+    bs.source_dir = testdata / proj
     bs.package = None
     bs.verbose = True
     bs.finalize()
@@ -177,9 +177,9 @@ def test_build_test_project(build_state, proj, configuration):
 
 
 @pytest.mark.parametrize("configuration", ["Debug", "Release"])
-def test_dllpack(build_state, configuration):
+def test_dllpack(build_state, testdata, configuration):
     bs = build_state
-    bs.source_dir = bs.source_dir.parent / "testdllpack"
+    bs.source_dir = testdata / "testdllpack"
     bs.package = None
     bs.verbose = True
     bs.finalize()
@@ -195,12 +195,12 @@ def test_dllpack(build_state, configuration):
 @pytest.mark.parametrize("configuration", ["Debug", "Release"])
 @pytest.mark.parametrize("encrypt", [b"a-bytes-key-0123", "a-str-key-01234567890123"])
 @pytest.mark.skipif(sys.platform not in {"win32"}, reason="Only supported on Windows")
-def test_dllpack_encrypted(build_state, configuration, encrypt):
+def test_dllpack_encrypted(build_state, testdata, configuration, encrypt):
     if not isinstance(encrypt, str):
         import base64
         encrypt = "base64:" + base64.b64encode(encrypt).decode("ascii")
     bs = build_state
-    bs.source_dir = bs.source_dir.parent / "testdllpack"
+    bs.source_dir = testdata / "testdllpack"
     bs.package = None
     bs.verbose = True
     bs.finalize()
diff --git a/tests/test_define.py b/tests/test_define.py
index 5b67347..8ef27e3 100644
--- a/tests/test_define.py
+++ b/tests/test_define.py
@@ -25,6 +25,8 @@ def test_iterable_project():
         T.PydFile.DefaultToolsetImports,
         T.PydFile.ConfigurationProperties,
         T.PydFile.ToolsetImports,
+        T.PydFile.TargetExtProperty,
+        T.PydFile.ToolsetPydProps,
         T.CSourceFile,
         T.PydFile.ToolsetTargets,
     ] == [type(p) for p in package]
diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py
new file mode 100644
index 0000000..619f1e9
--- /dev/null
+++ b/tests/test_entrypoint.py
@@ -0,0 +1,58 @@
+import os
+import pathlib
+import pytest
+import subprocess
+import sys
+
+PYMSBUILD_ROOT = os.path.dirname(os.path.dirname(__file__))
+sys.path.insert(0, PYMSBUILD_ROOT)
+
+from pymsbuild._build import locate_msbuild
+
+# Avoid calling locate() for each test
+if not os.getenv("MSBUILD"):
+    os.environ["MSBUILD"] = " ".join(locate_msbuild())
+    if os.environ["MSBUILD"] != " ".join(locate_msbuild()):
+        # We can't avoid it for some reason...
+        del os.environ["MSBUILD"]
+
+
+def run_build(cwd, tmp_path, config=None, env=None):
+    root = tmp_path / "build"
+    layout = tmp_path / "layout"
+    env = {
+        **os.environ,
+        "PYMSBUILD_FORCE": "1",
+        "PYMSBUILD_VERBOSE": "1",
+        "PYMSBUILD_TEMP_DIR": str(root),
+        "_PYMSBUILD_SOURCE_LAYOUT_DIR": str(layout),
+        "PYTHONPATH": PYMSBUILD_ROOT,
+        **(env if env else {}),
+    }
+    if config:
+        env["PYMSBUILD_CONFIG"] = str(config)
+
+    subprocess.check_call(
+        [sys.executable, "-m", "pymsbuild"],
+        env=env,
+        cwd=str(cwd),
+    )
+    built = [p.relative_to(layout) for p in layout.rglob("*") if p.is_file()]
+    print("Files:")
+    print(*(f" * {p}" for p in built), sep="\n")
+    return built
+
+
+@pytest.mark.parametrize("configuration", ["Debug", "Release"])
+@pytest.mark.skipif(sys.platform not in {"win32"}, reason="Only supported on Windows")
+def test_entry(testdata, tmp_path, configuration):
+    built = run_build(
+        testdata / "testentry",
+        tmp_path,
+        env={
+            "PYMSBUILD_CONFIGURATION": configuration,
+        },
+    )
+    assert pathlib.Path("testentry/run.exe") in built, "did not find run.exe"
+    assert any(p.match("testentry/python3*.dll") for p in built), "did not find python3*.dll"
+    assert any(p.match("testentry/files/app.*.pyd") for p in built), "did not find app.*.pyd"
diff --git a/tests/test_generate.py b/tests/test_generate.py
index 89043cd..d599f3a 100644
--- a/tests/test_generate.py
+++ b/tests/test_generate.py
@@ -57,7 +57,7 @@ def test_pyd_generation(tmp_path):
         T.SourceFile("m.txt"),
         TargetExt=".pyd",
     )
-    pf = ProjectFileChecker(G._generate_pyd(p, tmp_path, tmp_path))
+    pf = ProjectFileChecker(G._generate_c_project(p, tmp_path, tmp_path))
     targets = Path(pf.get("./x:PropertyGroup[@Label='Globals']/x:PyMsbuildTargets").text)
     assert (targets / "package.targets").is_file()
     assert (targets / "pyd.targets").is_file()
@@ -110,8 +110,6 @@ def test_package_project_reference(tmp_path):
 
     assert "package/module" == pf.get("./x:ItemGroup/x:Project[@Include='module.proj']/x:Name").text
     assert "package" == pf.get("./x:ItemGroup/x:Project[@Include='module.proj']/x:TargetDir").text
-    assert "module.TAG" == pf.get("./x:ItemGroup/x:Project[@Include='module.proj']/x:TargetName").text
-    assert ".pyd" == pf.get("./x:ItemGroup/x:Project[@Include='module.proj']/x:TargetExt").text
 
 
 def test_pkginfo_gen_readback(tmp_path):
diff --git a/tests/test_init.py b/tests/test_init.py
index 457661e..55db253 100644
--- a/tests/test_init.py
+++ b/tests/test_init.py
@@ -11,6 +11,15 @@
 from pymsbuild._build import BuildState
 from pymsbuild._init import run
 
+if sys.version_info >= (3, 12):
+    def ast_Str(n):
+        assert isinstance(n, ast.Constant)
+        return n.value
+else:
+    def ast_Str(n):
+        assert isinstance(n, ast.Str)
+        return n.s
+
 @pytest.fixture
 def init_project(inittestprojects, tmp_path):
     def do_init(name):
@@ -43,26 +52,25 @@ def visit_Assign(self, n):
     def visit_METADATA(self, n):
         assert isinstance(n, ast.Dict)
         for k, v in zip(n.keys, n.values):
-            assert isinstance(k, (ast.Constant, ast.Str))
-            if isinstance(k, ast.Str):
+            if isinstance(k, ast.Constant):
                 try:
-                    expect = self.metadata[k.s]
+                    expect = self.metadata[k.value]
                 except LookupError:
                     pass
                 else:
-                    assert expect == v.s
+                    assert expect == v.value
             else:
+                assert isinstance(k, ast.Str)
                 try:
-                    expect = self.metadata[k.value]
+                    expect = self.metadata[k.s]
                 except LookupError:
                     pass
                 else:
-                    assert expect == v.value
+                    assert expect == v.s
 
     def visit_PACKAGE(self, n, path):
         assert isinstance(n, ast.Call)
-        assert isinstance(n.args[0], ast.Str)
-        name = n.args[0].s
+        name = ast_Str(n.args[0])
         path = f"{path}/{n.func.id}[{name}]"
         self._package.append(path)
         for a in n.args[1:]:
diff --git a/tests/test_samples.py b/tests/test_samples.py
index bdc9e73..6be4438 100644
--- a/tests/test_samples.py
+++ b/tests/test_samples.py
@@ -4,8 +4,10 @@
 import shutil
 import subprocess
 import sys
+import urllib.error
 
 from pathlib import Path
+from urllib.request import urlopen
 
 ROOT = Path(__file__).absolute().parent.parent
 
@@ -20,14 +22,18 @@
     "PIP_REQUIRE_VIRTUALENV": "false",
     "PIP_VERBOSE": "true",
     "PYMSBUILD_VERBOSE": "true",
+    "GITHUB_REF": "",
 }
 
 WIN32_SAMPLES = {
     "azure-pack",
+    "azure-cli",
 }
 
 
-POSIX_SAMPLES = set()
+POSIX_SAMPLES = {
+    "azure-cli",
+}
 
 def all_samples(f):
     return pytest.mark.parametrize(
@@ -43,6 +49,44 @@ def maybe_skip(sample):
         pytest.skip("Windows-only sample")
 
 
+@pytest.fixture(scope="session")
+def with_simpleindex(tmp_path_factory):
+    TMP = tmp_path_factory.mktemp("with_simpleindex")
+    OUT = TMP / "wheelhouse"
+    subprocess.check_call(
+        [sys.executable, "-m", "pip", "wheel", ROOT, "-w", OUT],
+        cwd=ROOT,
+        env={
+            **ENV,
+            # Lie about the version number so we always select our build
+            "BUILD_BUILDNUMBER": "9999.0.0",
+            "PYMSBUILD_TEMP_DIR": str(TMP / "pymsbuild_tmp"),
+        },
+    )
+    shutil.copy2(ROOT / "tests/simpleindex.toml.in", OUT / "simpleindex.toml")
+    si_env = {"PIP_INDEX_URL": "http://127.0.0.1:65432/", "PIP_TRUSTED_HOST": "127.0.0.1"}
+    si_proc = subprocess.Popen(
+        [sys.executable, "-m", "simpleindex", OUT / "simpleindex.toml"],
+        cwd=OUT,
+        env={**ENV, **si_env},
+    )
+    try:
+        try:
+            si_proc.wait(1)
+        except subprocess.TimeoutExpired:
+            pass
+        urlopen(si_env["PIP_INDEX_URL"] + "pymsbuild", timeout=5.0).close()
+        yield si_env
+    except urllib.error.URLError:
+        try:
+            si_proc.wait(5)
+        except Exception:
+            pass
+        raise
+    finally:
+        si_proc.kill()
+
+
 def fresh_copy(sample, tmp_path):
     DIR = ROOT / "samples" / sample
     TMP = tmp_path / sample
@@ -65,10 +109,11 @@ def test_sample_build_inplace(sample, tmp_path):
     orig_files = {f.relative_to(DIR) for f in DIR.rglob("*")}
     env = dict(ENV)
 
-    if (DIR / "requirements.txt").is_file():
+    reqs = DIR / f"requirements-{sys.platform}.txt"
+    if reqs.is_file():
         PACKAGES = tmp_path / "packages"
         subprocess.check_call(
-            [sys.executable, "-m", "pip", "install", "-r", DIR / "requirements.txt", "--target", PACKAGES],
+            [sys.executable, "-m", "pip", "install", "-r", reqs, "--target", PACKAGES],
             env=env,
         )
         env["PYTHONPATH"] = os.pathsep.join(str(s) for s in [PACKAGES, env["PYTHONPATH"]] if s)
@@ -104,52 +149,52 @@ def test_sample_sdist(sample, tmp_path):
     assert dist
 
 @all_samples
-def test_sample_build_sdist(sample, tmp_path):
+def test_sample_build_sdist(sample, tmp_path, with_simpleindex):
     maybe_skip(sample)
     DIR = ROOT / "samples" / sample
     OUT = tmp_path / "out"
     subprocess.check_call(
         [sys.executable, "-m", "build", "--sdist", "-o", OUT],
         cwd=DIR,
-        env=ENV,
+        env={**ENV, **with_simpleindex},
     )
     dist = set(OUT.glob("*.tar.gz"))
     assert dist
 
 @all_samples
-def test_sample_build_wheel(sample, tmp_path):
+def test_sample_build_wheel(sample, tmp_path, with_simpleindex):
     maybe_skip(sample)
     DIR = ROOT / "samples" / sample
     OUT = tmp_path / "out"
     subprocess.check_call(
         [sys.executable, "-m", "build", "--wheel", "-o", OUT],
         cwd=DIR,
-        env=ENV,
+        env={**ENV, **with_simpleindex},
     )
     dist = set(OUT.glob("*.whl"))
     assert dist
 
 @all_samples
-def test_sample_pip_wheel(sample, tmp_path):
+def test_sample_pip_wheel(sample, tmp_path, with_simpleindex):
     maybe_skip(sample)
     DIR = ROOT / "samples" / sample
     OUT = tmp_path / "out"
     subprocess.check_call(
         [sys.executable, "-m", "pip", "wheel", DIR, "-w", OUT],
-        env=ENV,
+        env={**ENV, **with_simpleindex},
     )
     dist = set(OUT.glob("*.whl"))
     assert dist
 
 @all_samples
-def test_sample_pip_from_sdist(sample, tmp_path):
+def test_sample_pip_from_sdist(sample, tmp_path, with_simpleindex):
     maybe_skip(sample)
     DIR = ROOT / "samples" / sample
     OUT = tmp_path / "out"
     subprocess.check_call(
         [sys.executable, "-m", "build", "--sdist", "-o", OUT],
         cwd=DIR,
-        env=ENV,
+        env={**ENV, **with_simpleindex},
     )
     dist = set(OUT.glob("*.tar.gz"))
     assert dist
@@ -157,7 +202,7 @@ def test_sample_pip_from_sdist(sample, tmp_path):
         subprocess.check_call(
             [sys.executable, "-m", "pip", "wheel", d, "-w", OUT],
             cwd=DIR,
-            env=ENV,
+            env={**ENV, **with_simpleindex},
         )
         whls = set(OUT.glob("*.whl"))
         assert whls
diff --git a/tests/testcython/src.pyx b/tests/testcython/src.pyx
index be898dd..42abebd 100644
--- a/tests/testcython/src.pyx
+++ b/tests/testcython/src.pyx
@@ -3,5 +3,4 @@
 
 cimport header
 
-IF SHOW:
-    print("Made a module")
+print("Made a module")
diff --git a/tests/testdllpack/test-dllpack.py b/tests/testdllpack/test-dllpack.py
index a42bccb..734a540 100644
--- a/tests/testdllpack/test-dllpack.py
+++ b/tests/testdllpack/test-dllpack.py
@@ -38,7 +38,8 @@ def event_hook(event_name, args):
 
 assert TDP.__spec__.origin == TDP.__file__
 assert TDP.__spec__.name == "testdllpack"
-assert TDP.__spec__.submodule_search_locations == TDP.__path__ == []
+assert TDP.__spec__.submodule_search_locations == ["$dllpack:testdllpack"], TDP.__spec__.submodule_search_locations
+assert TDP.__spec__.submodule_search_locations == TDP.__path__, TDP.__path__
 assert TDP.INIT_MEMBER
 
 #######################################
diff --git a/tests/testentry/.gitignore b/tests/testentry/.gitignore
new file mode 100644
index 0000000..7d58ba0
--- /dev/null
+++ b/tests/testentry/.gitignore
@@ -0,0 +1,3 @@
+build/
+dist/
+testentry/
diff --git a/tests/testentry/Globe1.ico b/tests/testentry/Globe1.ico
new file mode 100644
index 0000000..7be1379
Binary files /dev/null and b/tests/testentry/Globe1.ico differ
diff --git a/tests/testentry/_msbuild.py b/tests/testentry/_msbuild.py
new file mode 100644
index 0000000..82f9ab4
--- /dev/null
+++ b/tests/testentry/_msbuild.py
@@ -0,0 +1,34 @@
+from pymsbuild import *
+from pymsbuild.dllpack import *
+from pymsbuild.entrypoint import *
+
+PACKAGE = Package(
+    "testentry",
+    Entrypoint(
+        "run", "app", "main",
+        VersionInfo(
+            LegalCopyright="Copyright Me",
+        ),
+        Icon("Globe1.ico"),
+        SearchPath("."),
+        SearchPath("stdlib.zip"),
+        SearchPath("files"),
+    ),
+    Package("files",
+        DllPackage(
+            "app",
+            PyFile("app.py", name="__init__.py"),
+        ),
+    ),
+)
+
+METADATA = {
+    "Name": "testentry",
+    "Version": "1.0.0",
+    "Author": "Test Author",
+    "Summary": "testentry project",
+}
+
+def init_METADATA():
+    for vi in PACKAGE.findall("*/VersionInfo"):
+        vi.from_metadata(METADATA)
diff --git a/tests/testentry/app.py b/tests/testentry/app.py
new file mode 100644
index 0000000..637fd4a
--- /dev/null
+++ b/tests/testentry/app.py
@@ -0,0 +1,16 @@
+import sys
+import _winapi
+
+def main():
+    print("Invoked with an entry point")
+    print("Executable:", sys.executable)
+    print("Args:", sys.argv[1:])
+    if sys.executable != sys.argv[0]:
+        print("WARNING: executable does not match argv[0]")
+    print("Prefix:", sys.prefix)
+    print("sys.path:", *sys.path, sep="\n- ")
+    print("DLL:", _winapi.GetModuleFileName(sys.dllhandle))
+
+if __name__ == "__main__":
+    print("Invoked directly")
+    sys.exit(2)