Implement Windows release builds in Azure Pipelines (GH-14065)

diff --git a/PC/crtlicense.txt b/PC/crtlicense.txt
new file mode 100644
index 0000000..f86841f
--- /dev/null
+++ b/PC/crtlicense.txt
@@ -0,0 +1,41 @@
+
+
+Additional Conditions for this Windows binary build
+---------------------------------------------------
+
+This program is linked with and uses Microsoft Distributable Code,
+copyrighted by Microsoft Corporation. The Microsoft Distributable Code
+is embedded in each .exe, .dll and .pyd file as a result of running
+the code through a linker.
+
+If you further distribute programs that include the Microsoft
+Distributable Code, you must comply with the restrictions on
+distribution specified by Microsoft. In particular, you must require
+distributors and external end users to agree to terms that protect the
+Microsoft Distributable Code at least as much as Microsoft's own
+requirements for the Distributable Code. See Microsoft's documentation
+(included in its developer tools and on its website at microsoft.com)
+for specific details.
+
+Redistribution of the Windows binary build of the Python interpreter
+complies with this agreement, provided that you do not:
+
+- alter any copyright, trademark or patent notice in Microsoft's
+Distributable Code;
+
+- use Microsoft's trademarks in your programs' names or in a way that
+suggests your programs come from or are endorsed by Microsoft;
+
+- distribute Microsoft's Distributable Code to run on a platform other
+than Microsoft operating systems, run-time technologies or application
+platforms; or
+
+- include Microsoft Distributable Code in malicious, deceptive or
+unlawful programs.
+
+These restrictions apply only to the Microsoft Distributable Code as
+defined above, not to Python itself or any programs running on the
+Python interpreter. The redistribution of the Python interpreter and
+libraries is governed by the Python Software License included with this
+file, or by other licenses as marked.
+
diff --git a/PC/layout/main.py b/PC/layout/main.py
index 624033e..c39aab2 100644
--- a/PC/layout/main.py
+++ b/PC/layout/main.py
@@ -31,6 +31,7 @@
 from .support.options import *
 from .support.pip import *
 from .support.props import *
+from .support.nuspec import *
 
 BDIST_WININST_FILES_ONLY = FileNameSet("wininst-*", "bdist_wininst.py")
 BDIST_WININST_STUB = "PC/layout/support/distutils.command.bdist_wininst.py"
@@ -66,6 +67,7 @@
 TOOLS_DIRS = FileNameSet("scripts", "i18n", "pynche", "demo", "parser")
 TOOLS_FILES = FileSuffixSet(".py", ".pyw", ".txt")
 
+
 def copy_if_modified(src, dest):
     try:
         dest_stat = os.stat(dest)
@@ -73,12 +75,15 @@
         do_copy = True
     else:
         src_stat = os.stat(src)
-        do_copy = (src_stat.st_mtime != dest_stat.st_mtime or
-                   src_stat.st_size != dest_stat.st_size)
+        do_copy = (
+            src_stat.st_mtime != dest_stat.st_mtime
+            or src_stat.st_size != dest_stat.st_size
+        )
 
     if do_copy:
         shutil.copy2(src, dest)
 
+
 def get_lib_layout(ns):
     def _c(f):
         if f in EXCLUDE_FROM_LIB:
@@ -119,7 +124,7 @@
         except FileNotFoundError:
             pass
         if not tcl_lib or not os.path.isdir(tcl_lib):
-            warn("Failed to find TCL_LIBRARY")
+            log_warning("Failed to find TCL_LIBRARY")
             return
 
     for dest, src in rglob(Path(tcl_lib).parent, "**/*"):
@@ -168,7 +173,7 @@
     for dest, src in rglob(ns.build, "vcruntime*.dll"):
         yield dest, src
 
-    yield "LICENSE.txt", ns.source / "LICENSE"
+    yield "LICENSE.txt", ns.build / "LICENSE.txt"
 
     for dest, src in rglob(ns.build, ("*.pyd", "*.dll")):
         if src.stem.endswith("_d") != bool(ns.debug) and src not in REQUIRED_DLLS:
@@ -222,15 +227,12 @@
         yield dest, src
 
     if ns.include_pip:
-        pip_dir = get_pip_dir(ns)
-        if not pip_dir.is_dir():
-            log_warning("Failed to find {} - pip will not be included", pip_dir)
-        else:
-            pkg_root = "packages/{}" if ns.zip_lib else "Lib/site-packages/{}"
-            for dest, src in rglob(pip_dir, "**/*"):
-                if src in EXCLUDE_FROM_LIB or src in EXCLUDE_FROM_PACKAGED_LIB:
-                    continue
-                yield pkg_root.format(dest), src
+        for dest, src in get_pip_layout(ns):
+            if isinstance(src, tuple) or not (
+                src in EXCLUDE_FROM_LIB or src in EXCLUDE_FROM_PACKAGED_LIB
+            ):
+                continue
+            yield dest, src
 
     if ns.include_chm:
         for dest, src in rglob(ns.doc_build / "htmlhelp", PYTHON_CHM_NAME):
@@ -244,6 +246,10 @@
         for dest, src in get_props_layout(ns):
             yield dest, src
 
+    if ns.include_nuspec:
+        for dest, src in get_nuspec_layout(ns):
+            yield dest, src
+
     for dest, src in get_appx_layout(ns):
         yield dest, src
 
@@ -287,7 +293,9 @@
         return None
 
     dest = (dest_dir or ns.temp) / (src.stem + ".py")
-    return _compile_one_py(src, dest.with_suffix(".pyc"), dest, optimize=2, checked=checked)
+    return _compile_one_py(
+        src, dest.with_suffix(".pyc"), dest, optimize=2, checked=checked
+    )
 
 
 def _write_to_zip(zf, dest, src, ns, checked=True):
@@ -361,28 +369,9 @@
             print("# Uncomment to run site.main() automatically", file=f)
             print("#import site", file=f)
 
-    if ns.include_appxmanifest:
-        log_info("Generating AppxManifest.xml in {}", ns.temp)
-        ns.temp.mkdir(parents=True, exist_ok=True)
-
-        with open(ns.temp / "AppxManifest.xml", "wb") as f:
-            f.write(get_appxmanifest(ns))
-
-        with open(ns.temp / "_resources.xml", "wb") as f:
-            f.write(get_resources_xml(ns))
-
     if ns.include_pip:
-        pip_dir = get_pip_dir(ns)
-        if not (pip_dir / "pip").is_dir():
-            log_info("Extracting pip to {}", pip_dir)
-            pip_dir.mkdir(parents=True, exist_ok=True)
-            extract_pip_files(ns)
-
-    if ns.include_props:
-        log_info("Generating {} in {}", PYTHON_PROPS_NAME, ns.temp)
-        ns.temp.mkdir(parents=True, exist_ok=True)
-        with open(ns.temp / PYTHON_PROPS_NAME, "wb") as f:
-            f.write(get_props(ns))
+        log_info("Extracting pip")
+        extract_pip_files(ns)
 
 
 def _create_zip_file(ns):
@@ -427,6 +416,18 @@
                     log_info("Processed {} files", count)
             log_debug("Processing {!s}", src)
 
+            if isinstance(src, tuple):
+                src, content = src
+                if ns.copy:
+                    log_debug("Copy {} -> {}", src, ns.copy / dest)
+                    (ns.copy / dest).parent.mkdir(parents=True, exist_ok=True)
+                    with open(ns.copy / dest, "wb") as f:
+                        f.write(content)
+                if ns.zip:
+                    log_debug("Zip {} into {}", src, ns.zip)
+                    zip_file.writestr(str(dest), content)
+                continue
+
             if (
                 ns.precompile
                 and src in PY_FILES
diff --git a/PC/layout/support/appxmanifest.py b/PC/layout/support/appxmanifest.py
index 49a35fa..58fba84 100644
--- a/PC/layout/support/appxmanifest.py
+++ b/PC/layout/support/appxmanifest.py
@@ -17,12 +17,7 @@
 
 from .constants import *
 
-__all__ = []
-
-
-def public(f):
-    __all__.append(f.__name__)
-    return f
+__all__ = ["get_appx_layout"]
 
 
 APPX_DATA = dict(
@@ -166,9 +161,7 @@
             "Help": {
                 "Main Python Documentation": {
                     "_condition": lambda ns: ns.include_chm,
-                    "": "[{{AppVPackageRoot}}]\\Doc\\{}".format(
-                        PYTHON_CHM_NAME
-                    ),
+                    "": "[{{AppVPackageRoot}}]\\Doc\\{}".format(PYTHON_CHM_NAME),
                 },
                 "Local Python Documentation": {
                     "_condition": lambda ns: ns.include_html_doc,
@@ -239,31 +232,6 @@
     return sccd
 
 
-@public
-def get_appx_layout(ns):
-    if not ns.include_appxmanifest:
-        return
-
-    yield "AppxManifest.xml", ns.temp / "AppxManifest.xml"
-    yield "_resources.xml", ns.temp / "_resources.xml"
-    icons = ns.source / "PC" / "icons"
-    yield "_resources/pythonx44.png", icons / "pythonx44.png"
-    yield "_resources/pythonx44$targetsize-44_altform-unplated.png", icons / "pythonx44.png"
-    yield "_resources/pythonx50.png", icons / "pythonx50.png"
-    yield "_resources/pythonx50$targetsize-50_altform-unplated.png", icons / "pythonx50.png"
-    yield "_resources/pythonx150.png", icons / "pythonx150.png"
-    yield "_resources/pythonx150$targetsize-150_altform-unplated.png", icons / "pythonx150.png"
-    yield "_resources/pythonwx44.png", icons / "pythonwx44.png"
-    yield "_resources/pythonwx44$targetsize-44_altform-unplated.png", icons / "pythonwx44.png"
-    yield "_resources/pythonwx150.png", icons / "pythonwx150.png"
-    yield "_resources/pythonwx150$targetsize-150_altform-unplated.png", icons / "pythonwx150.png"
-    sccd = ns.source / SCCD_FILENAME
-    if sccd.is_file():
-        # This should only be set for side-loading purposes.
-        sccd = _fixup_sccd(ns, sccd, os.getenv("APPX_DATA_SHA256"))
-        yield sccd.name, sccd
-
-
 def find_or_add(xml, element, attr=None, always_add=False):
     if always_add:
         e = None
@@ -393,7 +361,6 @@
     e = find_or_add(e, "rescap:Capability", ("Name", "unvirtualizedResources"))
 
 
-@public
 def get_appxmanifest(ns):
     for k, v in APPXMANIFEST_NS.items():
         ET.register_namespace(k, v)
@@ -481,6 +448,29 @@
     return buffer.getbuffer()
 
 
-@public
 def get_resources_xml(ns):
     return RESOURCES_XML_TEMPLATE.encode("utf-8")
+
+
+def get_appx_layout(ns):
+    if not ns.include_appxmanifest:
+        return
+
+    yield "AppxManifest.xml", ("AppxManifest.xml", get_appxmanifest(ns))
+    yield "_resources.xml", ("_resources.xml", get_resources_xml(ns))
+    icons = ns.source / "PC" / "icons"
+    yield "_resources/pythonx44.png", icons / "pythonx44.png"
+    yield "_resources/pythonx44$targetsize-44_altform-unplated.png", icons / "pythonx44.png"
+    yield "_resources/pythonx50.png", icons / "pythonx50.png"
+    yield "_resources/pythonx50$targetsize-50_altform-unplated.png", icons / "pythonx50.png"
+    yield "_resources/pythonx150.png", icons / "pythonx150.png"
+    yield "_resources/pythonx150$targetsize-150_altform-unplated.png", icons / "pythonx150.png"
+    yield "_resources/pythonwx44.png", icons / "pythonwx44.png"
+    yield "_resources/pythonwx44$targetsize-44_altform-unplated.png", icons / "pythonwx44.png"
+    yield "_resources/pythonwx150.png", icons / "pythonwx150.png"
+    yield "_resources/pythonwx150$targetsize-150_altform-unplated.png", icons / "pythonwx150.png"
+    sccd = ns.source / SCCD_FILENAME
+    if sccd.is_file():
+        # This should only be set for side-loading purposes.
+        sccd = _fixup_sccd(ns, sccd, os.getenv("APPX_DATA_SHA256"))
+        yield sccd.name, sccd
diff --git a/PC/layout/support/nuspec.py b/PC/layout/support/nuspec.py
new file mode 100644
index 0000000..ba26ff3
--- /dev/null
+++ b/PC/layout/support/nuspec.py
@@ -0,0 +1,66 @@
+"""
+Provides .props file.
+"""
+
+import os
+
+from .constants import *
+
+__all__ = ["get_nuspec_layout"]
+
+PYTHON_NUSPEC_NAME = "python.nuspec"
+
+NUSPEC_DATA = {
+    "PYTHON_TAG": VER_DOT,
+    "PYTHON_VERSION": os.getenv("PYTHON_NUSPEC_VERSION"),
+    "PYTHON_BITNESS": "64-bit" if IS_X64 else "32-bit",
+    "PACKAGENAME": os.getenv("PYTHON_NUSPEC_PACKAGENAME"),
+    "PACKAGETITLE": os.getenv("PYTHON_NUSPEC_PACKAGETITLE"),
+    "FILELIST": r'    <file src="**\*" target="tools" />',
+}
+
+if not NUSPEC_DATA["PYTHON_VERSION"]:
+    if VER_NAME:
+        NUSPEC_DATA["PYTHON_VERSION"] = "{}.{}-{}{}".format(
+            VER_DOT, VER_MICRO, VER_NAME, VER_SERIAL
+        )
+    else:
+        NUSPEC_DATA["PYTHON_VERSION"] = "{}.{}".format(VER_DOT, VER_MICRO)
+
+if not NUSPEC_DATA["PACKAGETITLE"]:
+    NUSPEC_DATA["PACKAGETITLE"] = "Python" if IS_X64 else "Python (32-bit)"
+
+if not NUSPEC_DATA["PACKAGENAME"]:
+    NUSPEC_DATA["PACKAGENAME"] = "python" if IS_X64 else "pythonx86"
+
+FILELIST_WITH_PROPS = r"""    <file src="**\*" exclude="python.props" target="tools" />
+    <file src="python.props" target="build\native" />"""
+
+NUSPEC_TEMPLATE = r"""<?xml version="1.0"?>
+<package>
+  <metadata>
+    <id>{PACKAGENAME}</id>
+    <title>{PACKAGETITLE}</title>
+    <version>{PYTHON_VERSION}</version>
+    <authors>Python Software Foundation</authors>
+    <license type="file">tools\LICENSE.txt</license>
+    <projectUrl>https://www.python.org/</projectUrl>
+    <description>Installs {PYTHON_BITNESS} Python for use in build scenarios.</description>
+    <iconUrl>https://www.python.org/static/favicon.ico</iconUrl>
+    <tags>python</tags>
+  </metadata>
+  <files>
+{FILELIST}
+  </files>
+</package>
+"""
+
+
+def get_nuspec_layout(ns):
+    if ns.include_all or ns.include_nuspec:
+        data = NUSPEC_DATA
+        if ns.include_all or ns.include_props:
+            data = dict(data)
+            data["FILELIST"] = FILELIST_WITH_PROPS
+        nuspec = NUSPEC_TEMPLATE.format_map(data)
+        yield "python.nuspec", ("python.nuspec", nuspec.encode("utf-8"))
diff --git a/PC/layout/support/options.py b/PC/layout/support/options.py
index 00f0566..c8ae4e3 100644
--- a/PC/layout/support/options.py
+++ b/PC/layout/support/options.py
@@ -30,6 +30,7 @@
     "launchers": {"help": "specific launchers"},
     "appxmanifest": {"help": "an appxmanifest"},
     "props": {"help": "a python.props file"},
+    "nuspec": {"help": "a python.nuspec file"},
     "chm": {"help": "the CHM documentation"},
     "html-doc": {"help": "the HTML documentation"},
 }
@@ -60,13 +61,11 @@
             "stable",
             "distutils",
             "venv",
-            "props"
+            "props",
+            "nuspec",
         ],
     },
-    "iot": {
-        "help": "Windows IoT Core",
-        "options": ["stable", "pip"],
-    },
+    "iot": {"help": "Windows IoT Core", "options": ["stable", "pip"]},
     "default": {
         "help": "development kit package",
         "options": [
diff --git a/PC/layout/support/pip.py b/PC/layout/support/pip.py
index 369a923..eada456 100644
--- a/PC/layout/support/pip.py
+++ b/PC/layout/support/pip.py
@@ -11,15 +11,11 @@
 import subprocess
 import sys
 
-__all__ = []
+from .filesets import *
+
+__all__ = ["extract_pip_files", "get_pip_layout"]
 
 
-def public(f):
-    __all__.append(f.__name__)
-    return f
-
-
-@public
 def get_pip_dir(ns):
     if ns.copy:
         if ns.zip_lib:
@@ -29,10 +25,23 @@
         return ns.temp / "packages"
 
 
-@public
+def get_pip_layout(ns):
+    pip_dir = get_pip_dir(ns)
+    if not pip_dir.is_dir():
+        log_warning("Failed to find {} - pip will not be included", pip_dir)
+    else:
+        pkg_root = "packages/{}" if ns.zip_lib else "Lib/site-packages/{}"
+        for dest, src in rglob(pip_dir, "**/*"):
+            yield pkg_root.format(dest), src
+        yield "pip.ini", ("pip.ini", b"[global]\nuser=yes")
+
+
 def extract_pip_files(ns):
     dest = get_pip_dir(ns)
-    dest.mkdir(parents=True, exist_ok=True)
+    try:
+        dest.mkdir(parents=True, exist_ok=False)
+    except IOError:
+        return
 
     src = ns.source / "Lib" / "ensurepip" / "_bundled"
 
@@ -58,6 +67,7 @@
             "--target",
             str(dest),
             "--no-index",
+            "--no-compile",
             "--no-cache-dir",
             "-f",
             str(src),
diff --git a/PC/layout/support/props.py b/PC/layout/support/props.py
index 3a047d2..4d3b061 100644
--- a/PC/layout/support/props.py
+++ b/PC/layout/support/props.py
@@ -6,13 +6,7 @@
 
 from .constants import *
 
-__all__ = ["PYTHON_PROPS_NAME"]
-
-
-def public(f):
-    __all__.append(f.__name__)
-    return f
-
+__all__ = ["get_props_layout"]
 
 PYTHON_PROPS_NAME = "python.props"
 
@@ -97,14 +91,8 @@
 """
 
 
-@public
 def get_props_layout(ns):
     if ns.include_all or ns.include_props:
-        yield "python.props", ns.temp / "python.props"
-
-
-@public
-def get_props(ns):
-    # TODO: Filter contents of props file according to included/excluded items
-    props = PROPS_TEMPLATE.format_map(PROPS_DATA)
-    return props.encode("utf-8")
+        # TODO: Filter contents of props file according to included/excluded items
+        props = PROPS_TEMPLATE.format_map(PROPS_DATA)
+        yield "python.props", ("python.props", props.encode("utf-8"))
diff --git a/PC/python_uwp.cpp b/PC/python_uwp.cpp
index 5c8caa6..dd1edde 100644
--- a/PC/python_uwp.cpp
+++ b/PC/python_uwp.cpp
@@ -182,9 +182,9 @@
             if (*p++ == L'\\') {
                 if (wcsnicmp(p, L"pip", 3) == 0) {
                     moduleName = L"pip";
+                    /* No longer required when pip 19.1 is added */
                     _wputenv_s(L"PIP_USER", L"true");
-                }
-                else if (wcsnicmp(p, L"idle", 4) == 0) {
+                } else if (wcsnicmp(p, L"idle", 4) == 0) {
                     moduleName = L"idlelib";
                 }
             }