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

diff --git a/Tools/msi/buildrelease.bat b/Tools/msi/buildrelease.bat
index 45e189b..b72eede 100644
--- a/Tools/msi/buildrelease.bat
+++ b/Tools/msi/buildrelease.bat
@@ -29,7 +29,7 @@
 
 set D=%~dp0
 set PCBUILD=%D%..\..\PCbuild\
-if "%Py_OutDir%"=="" set Py_OutDir=%PCBUILD%
+if NOT DEFINED Py_OutDir set Py_OutDir=%PCBUILD%
 set EXTERNALS=%D%..\..\externals\windows-installer\
 
 set BUILDX86=
diff --git a/Tools/msi/exe/crtlicense.txt b/Tools/msi/exe/crtlicense.txt
deleted file mode 100644
index f86841f..0000000
--- a/Tools/msi/exe/crtlicense.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-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/Tools/msi/exe/exe.wixproj b/Tools/msi/exe/exe.wixproj
index 071501c..326766b 100644
--- a/Tools/msi/exe/exe.wixproj
+++ b/Tools/msi/exe/exe.wixproj
@@ -21,25 +21,6 @@
         <WxlTemplate Include="*.wxl_template" />
     </ItemGroup>
     
-    <Target Name="_GenerateLicense" AfterTargets="PrepareForBuild">
-        <ItemGroup>
-            <LicenseFiles Include="$(PySourcePath)LICENSE;
-                                   crtlicense.txt;
-                                   $(bz2Dir)LICENSE;
-                                   $(opensslOutDir)LICENSE;
-                                   $(tcltkDir)tcllicense.terms;
-                                   $(tcltkDir)tklicense.terms;
-                                   $(tcltkDir)tixlicense.terms" />
-            <_LicenseFiles Include="@(LicenseFiles)">
-                <Content>$([System.IO.File]::ReadAllText(%(FullPath)))</Content>
-            </_LicenseFiles>
-        </ItemGroup>
-        
-        <WriteLinesToFile File="$(BuildPath)LICENSE"
-                          Overwrite="true"
-                          Lines="@(_LicenseFiles->'%(Content)')" />
-    </Target>
-    
     <Target Name="_CopyMiscNews" AfterTargets="PrepareForBuild" Condition="Exists('$(PySourcePath)Misc\NEWS')">
         <Copy SourceFiles="$(PySourcePath)Misc\NEWS" DestinationFiles="$(BuildPath)NEWS.txt" />
     </Target>
diff --git a/Tools/msi/exe/exe_files.wxs b/Tools/msi/exe/exe_files.wxs
index 394b4de..483d06c 100644
--- a/Tools/msi/exe/exe_files.wxs
+++ b/Tools/msi/exe/exe_files.wxs
@@ -3,7 +3,7 @@
     <Fragment>
         <ComponentGroup Id="exe_txt">
             <Component Id="LICENSE.txt" Directory="InstallDirectory" Guid="*">
-                <File Name="LICENSE.txt" Source="LICENSE" KeyPath="yes" />
+                <File Name="LICENSE.txt" Source="LICENSE.txt" KeyPath="yes" />
             </Component>
             <Component Id="NEWS.txt" Directory="InstallDirectory" Guid="*">
                 <File Name="NEWS.txt" KeyPath="yes" />
diff --git a/Tools/msi/make_cat.ps1 b/Tools/msi/make_cat.ps1
index cc3cd4a..9ea3ddd 100644
--- a/Tools/msi/make_cat.ps1
+++ b/Tools/msi/make_cat.ps1
@@ -7,6 +7,8 @@
     The path to the catalog definition file to compile and
     sign. It is assumed that the .cat file will be the same
     name with a new extension.
+.Parameter outfile
+    The path to move the built .cat file to (optional).
 .Parameter description
     The description to add to the signature (optional).
 .Parameter certname
@@ -16,6 +18,7 @@
 #>
 param(
     [Parameter(Mandatory=$true)][string]$catalog,
+    [string]$outfile,
     [switch]$sign,
     [string]$description,
     [string]$certname,
@@ -35,3 +38,8 @@
 if ($sign) {
     Sign-File -certname $certname -certsha1 $certsha1 -certfile $certfile -description $description -files @($catalog -replace 'cdf$', 'cat')
 }
+
+if ($outfile) {
+    Split-Path -Parent $outfile | ?{ $_ } | %{ mkdir -Force $_; }
+    Move-Item ($catalog -replace 'cdf$', 'cat') $outfile
+}
diff --git a/Tools/msi/msi.props b/Tools/msi/msi.props
index 5da901c..3f14501 100644
--- a/Tools/msi/msi.props
+++ b/Tools/msi/msi.props
@@ -56,6 +56,7 @@
         <ReuseCabinetCache>true</ReuseCabinetCache>
         <CRTRedist Condition="'$(CRTRedist)' == ''">$(ExternalsDir)\windows-installer\redist-1\$(Platform)</CRTRedist>
         <CRTRedist>$([System.IO.Path]::GetFullPath($(CRTRedist)))</CRTRedist>
+        <TclTkLibraryDir Condition="$(TclTkLibraryDir) == ''">$(tcltkDir)lib</TclTkLibraryDir>
         <DocFilename>python$(MajorVersionNumber)$(MinorVersionNumber)$(MicroVersionNumber)$(ReleaseLevelName).chm</DocFilename>
 
         <InstallerVersion>$(MajorVersionNumber).$(MinorVersionNumber).$(Field3Value).0</InstallerVersion>
@@ -121,7 +122,7 @@
         <LinkerBindInputPaths Include="$(PySourcePath)">
             <BindName>src</BindName>
         </LinkerBindInputPaths>
-        <LinkerBindInputPaths Include="$(tcltkDir)">
+        <LinkerBindInputPaths Include="$(TclTkLibraryDir)">
             <BindName>tcltk</BindName>
         </LinkerBindInputPaths>
         <LinkerBindInputPaths Include="$(CRTRedist)">
diff --git a/Tools/msi/msi.targets b/Tools/msi/msi.targets
index 9283a1e..4788a63 100644
--- a/Tools/msi/msi.targets
+++ b/Tools/msi/msi.targets
@@ -47,7 +47,7 @@
 
         <WriteLinesToFile File="$(_CatFileSourceTarget)" Lines="$(_CatFile)" Overwrite="true" />
         <Exec Command='$(_MakeCatCommand) "$(_CatFileSourceTarget)"' WorkingDirectory="$(MSBuildThisFileDirectory)" />
-        <Exec Command='$(_SignCommand) "$(_CatFileTarget)"' WorkingDirectory="$(MSBuildThisFileDirectory)"
+        <Exec Command='$(_SignCommand) "$(_CatFileTarget)" || $(_SignCommand) "$(_CatFileTarget)" || $(_SignCommand) "$(_CatFileTarget)"' WorkingDirectory="$(MSBuildThisFileDirectory)"
               Condition="Exists($(_CatFileTarget)) and '$(_SignCommand)' != ''" />
 
         <ItemGroup>
@@ -76,18 +76,18 @@
 
     <Target Name="SignCabs">
         <Error Text="Unable to locate signtool.exe. Set /p:SignToolPath and rebuild" Condition="'$(_SignCommand)' == ''" />
-        <Exec Command="$(_SignCommand) @(SignCabs->'&quot;%(FullPath)&quot;',' ')" ContinueOnError="false" />
+        <Exec Command="$(_SignCommand) @(SignCabs->'&quot;%(FullPath)&quot;',' ') || $(_SignCommand) @(SignCabs->'&quot;%(FullPath)&quot;',' ') || $(_SignCommand) @(SignCabs->'&quot;%(FullPath)&quot;',' ')" ContinueOnError="false" />
     </Target>
     <Target Name="SignMsi">
         <Error Text="Unable to locate signtool.exe. Set /p:SignToolPath and rebuild" Condition="'$(_SignCommand)' == ''" />
-        <Exec Command="$(_SignCommand) @(SignMsi->'&quot;%(FullPath)&quot;',' ')" ContinueOnError="false" />
+        <Exec Command="$(_SignCommand) @(SignMsi->'&quot;%(FullPath)&quot;',' ') || $(_SignCommand) @(SignMsi->'&quot;%(FullPath)&quot;',' ') || $(_SignCommand) @(SignMsi->'&quot;%(FullPath)&quot;',' ')" ContinueOnError="false" />
     </Target>
     <Target Name="SignBundleEngine">
         <Error Text="Unable to locate signtool.exe. Set /p:SignToolPath and rebuild" Condition="'$(_SignCommand)' == ''" />
-        <Exec Command="$(_SignCommand) @(SignBundleEngine->'&quot;%(FullPath)&quot;',' ')" ContinueOnError="false" />
+        <Exec Command="$(_SignCommand) @(SignBundleEngine->'&quot;%(FullPath)&quot;',' ') || $(_SignCommand) @(SignBundleEngine->'&quot;%(FullPath)&quot;',' ') || $(_SignCommand) @(SignBundleEngine->'&quot;%(FullPath)&quot;',' ')" ContinueOnError="false" />
     </Target>
     <Target Name="SignBundle">
         <Error Text="Unable to locate signtool.exe. Set /p:SignToolPath and rebuild" Condition="'$(_SignCommand)' == ''" />
-        <Exec Command="$(_SignCommand) @(SignBundle->'&quot;%(FullPath)&quot;',' ')" ContinueOnError="false" />
+        <Exec Command="$(_SignCommand) @(SignBundle->'&quot;%(FullPath)&quot;',' ') || $(_SignCommand) @(SignBundle->'&quot;%(FullPath)&quot;',' ') || $(_SignCommand) @(SignBundle->'&quot;%(FullPath)&quot;',' ')" ContinueOnError="false" />
     </Target>
 </Project>
\ No newline at end of file
diff --git a/Tools/msi/sign_build.ps1 b/Tools/msi/sign_build.ps1
index 6668eb3..d3f7504 100644
--- a/Tools/msi/sign_build.ps1
+++ b/Tools/msi/sign_build.ps1
@@ -16,7 +16,7 @@
 #>
 param(
     [Parameter(Mandatory=$true)][string]$root,
-    [string[]]$patterns=@("*.exe", "*.dll", "*.pyd"),
+    [string[]]$patterns=@("*.exe", "*.dll", "*.pyd", "*.cat"),
     [string]$description,
     [string]$certname,
     [string]$certsha1,
diff --git a/Tools/msi/tcltk/tcltk.wixproj b/Tools/msi/tcltk/tcltk.wixproj
index fae353f..218f3d1 100644
--- a/Tools/msi/tcltk/tcltk.wixproj
+++ b/Tools/msi/tcltk/tcltk.wixproj
@@ -20,10 +20,10 @@
         <WxlTemplate Include="*.wxl_template" />
     </ItemGroup>
     <ItemGroup>
-        <InstallFiles Include="$(tcltkDir)lib\**\*">
-            <SourceBase>$(tcltkDir)</SourceBase>
+        <InstallFiles Include="$(TclTkLibraryDir)\**\*">
+            <SourceBase>$(TclTkLibraryDir)</SourceBase>
             <Source>!(bindpath.tcltk)</Source>
-            <TargetBase>$(tcltkDir)lib</TargetBase>
+            <TargetBase>$(TclTkLibraryDir)</TargetBase>
             <Target_>tcl\</Target_>
             <Group>tcltk_lib</Group>
         </InstallFiles>
diff --git a/Tools/msi/uploadrelease.ps1 b/Tools/msi/uploadrelease.ps1
index 491df80..b6fbeea 100644
--- a/Tools/msi/uploadrelease.ps1
+++ b/Tools/msi/uploadrelease.ps1
@@ -15,6 +15,10 @@
     The subdirectory on the host to copy files to.
 .Parameter tests
     The path to run download tests in.
+.Parameter doc_htmlhelp
+    Optional path besides -build to locate CHM files.
+.Parameter embed
+    Optional path besides -build to locate ZIP files.
 .Parameter skipupload
     Skip uploading
 .Parameter skippurge
@@ -30,6 +34,8 @@
     [string]$server="python-downloads",
     [string]$target="/srv/www.python.org/ftp/python",
     [string]$tests=${env:TEMP},
+    [string]$doc_htmlhelp=$null,
+    [string]$embed=$null,
     [switch]$skipupload,
     [switch]$skippurge,
     [switch]$skiptest,
@@ -73,32 +79,45 @@
     "Upload using $pscp and $plink"
     ""
 
-    pushd $build
-    $doc = gci python*.chm, python*.chm.asc
+    if ($doc_htmlhelp) {
+        pushd $doc_htmlhelp
+    } else {
+        pushd $build
+    }
+    $chm = gci python*.chm, python*.chm.asc
     popd
 
     $d = "$target/$($p[0])/"
     & $plink -batch $user@$server mkdir $d
     & $plink -batch $user@$server chgrp downloads $d
     & $plink -batch $user@$server chmod g-x,o+rx $d
-    & $pscp -batch $doc.FullName "$user@${server}:$d"
+    & $pscp -batch $chm.FullName "$user@${server}:$d"
 
-    foreach ($a in gci "$build" -Directory) {
+    $dirs = gci "$build" -Directory
+    if ($embed) {
+        $dirs = ($dirs, (gi $embed)) | %{ $_ }
+    }
+
+    foreach ($a in $dirs) {
         "Uploading files from $($a.FullName)"
         pushd "$($a.FullName)"
         $exe = gci *.exe, *.exe.asc, *.zip, *.zip.asc
         $msi = gci *.msi, *.msi.asc, *.msu, *.msu.asc
         popd
 
-        & $pscp -batch $exe.FullName "$user@${server}:$d"
+        if ($exe) {
+            & $pscp -batch $exe.FullName "$user@${server}:$d"
+        }
 
-        $sd = "$d$($a.Name)$($p[1])/"
-        & $plink -batch $user@$server mkdir $sd
-        & $plink -batch $user@$server chgrp downloads $sd
-        & $plink -batch $user@$server chmod g-x,o+rx $sd
-        & $pscp -batch $msi.FullName "$user@${server}:$sd"
-        & $plink -batch $user@$server chgrp downloads $sd*
-        & $plink -batch $user@$server chmod g-x,o+r $sd*
+        if ($msi) {
+            $sd = "$d$($a.Name)$($p[1])/"
+            & $plink -batch $user@$server mkdir $sd
+            & $plink -batch $user@$server chgrp downloads $sd
+            & $plink -batch $user@$server chmod g-x,o+rx $sd
+            & $pscp -batch $msi.FullName "$user@${server}:$sd"
+            & $plink -batch $user@$server chgrp downloads $sd*
+            & $plink -batch $user@$server chmod g-x,o+r $sd*
+        }
     }
 
     & $plink -batch $user@$server chgrp downloads $d*
@@ -128,7 +147,18 @@
 if (-not $skiphash) {
     # Display MD5 hash and size of each downloadable file
     pushd $build
-    $hashes = gci python*.chm, *\*.exe, *\*.zip | `
+    $files = gci python*.chm, *\*.exe, *\*.zip
+    if ($doc_htmlhelp) {
+        cd $doc_htmlhelp
+        $files = ($files, (gci python*.chm)) | %{ $_ }
+    }
+    if ($embed) {
+        cd $embed
+        $files = ($files, (gci *.zip)) | %{ $_ }
+    }
+    popd
+
+    $hashes = $files | `
         Sort-Object Name | `
         Format-Table Name, @{Label="MD5"; Expression={(Get-FileHash $_ -Algorithm MD5).Hash}}, Length -AutoSize | `
         Out-String -Width 4096