Skip to content

Commit f9d15dd

Browse files
jonathanpeppersjonpryor
authored andcommitted
[xabuild.exe] cross-platform form of tools/scripts/xabuild (#910)
Context: https://github.com/jonathanpeppers/xabuild What do we want? A *usable*, *parallel*, build tree. To elaborate: we want to be able to have a "system" Xamarin.Android install, and a "parallel" xamarin-android install, and be able to *easily* switch between the two. (For various definitions of "easy"; here, we mean *command-line* use, not IDE use.) On macOS, this (more or less) Just Works™, and is extremely handy for testing bug fixes: $ xbuild /t:Install Project.csproj # Verify that some bug is triggered $ xbuild /t:Uninstall Project.csproj $ tools/scripts/xabuild /t:Install Project.csproj # Verify that some bug is *fixed* There's One Problem™ with this: MSBuild does not make this easy. (Related: commit aa1db83.) Apps may rely on files located within `$(MSBuildExtensionsPath)` or `$(TargetFrameworkRootPath)`, files such as PCL profile assemblies, or 3rd party frameworks. Meanwhile, on macOS, `xabuild` is *predicated* upon overriding `$(TargetFrameworkRootPath)` and `$(MSBuildExtensionsPath)` and `$(XBUILD_FRAMEWORK_FOLDERS_PATH)`, and creating a bunch of symlinks to "fake out" `msbuild.exe` so that system-installed files such as PCL assemblies can be found *through* the parallel environment. It kinda/sorta works on macOS. It completely falls apart when using Windows. There is no easy "symlink half the world" solution there. Overriding `$(MSBuildExtensionsPath)` means that `Microsoft.Common.targets` can't be found. Overriding `$(TargetFrameworkRootPath)` means PCL files can't be found. It's a mess. Fortunately, more recent versions of MSBuild allow for some of these properties to contain *multiple* directories instead of a single directory, which means *there is a way* to support our desired usable parallel install world order. We "just" need to e.g. force `$(MSBuildExtensionsPath)` to contain *both* the in-tree directory *and* the system directory: ```xml <MSBuildExtensionsPath>In-Tree Directory; System Directory</MSBuildExtensionsPath> ``` Unfortunately, *this isn't easy*. Not all of these properties can be overridden on the `msbuild.exe` command line. Worse, MSBuild doesn't allow `;` to be part of a property value, as `;` is a property name [separator char](https://msdn.microsoft.com/en-us/library/ms164311.aspx) msbuild.exe /property:WarningLevel=2;OutDir=bin\Debug The way to force MSBuild to accept multiple paths in a property value is by providing a `.exe.config` file with the appropriate values. ~~ Enter `xabuild.exe` ~~ `xabuild.exe` is a nice wrapper around MSBuild for compiling Xamarin.Android projects using a locally built version of Xamarin.Android on your system. It seems to work on Windows, macOS, and Linux and doesn’t require elevation or modifications to your system. `xabuild.exe` works by doing the following: 1. Reference `MSBuild.exe` or `MSBuild.dll` depending on the platform 2. Creates symbolic links to `.NETPortable` and `.NETFramework` directories inside the Xamarin.Android build output directory 3. Overrides MSBuild's `app.config` file to set various properties, such as `$(MSBuildExtensionsPath)`. 4. Run MSBuild’s `Main()` method ~~ Usage ~~ On macOS, `tools/scripts/xabuild` has been updated to use `xabuild.exe` when `$MSBUILD` is `msbuild: $ MSBUILD=msbuild tools/scripts/xabuild /t:SignAndroidPackage samples/HelloWorld/HelloWorld.csproj When `$MSBUILD` is `xbuild` (the current default), the previous behavior of overriding `$MSBuildExtensionsPath` and `$XBUILD_FRAMEWORK_FOLDERS_PATH` is still used. On Windows, MSBuild 15.3 from Visual Studio 2017 is required. Simply execute `xabuild.exe`: > bin\Debug\bin\xabuild.exe Xamarin.Android-Tests.sln /p:XAIntegratedTests=False Before `xabuild.exe` existed, `setup-windows.exe` would need to be executed (as Administrator!) in order for `Xamarin.Android-Tests.sln` to be built using `msbuild.exe`.
1 parent f91b5d0 commit f9d15dd

File tree

11 files changed

+481
-92
lines changed

11 files changed

+481
-92
lines changed

Documentation/DevelopmentTips.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ to hold file locks on output assemblies containing MSBuild tasks. Until there is
116116
for this, it might be more advisable to use an editor like Visual Studio Code and build via
117117
the command-line.
118118

119+
Windows also requires `xabuild.exe` in place of the `tools/scripts/xabuild` script used
120+
on other platforms.
121+
122+
So a command on macOS such as:
123+
124+
$ tools/scripts/xabuild /t:SignAndroidPackage tests/locales/Xamarin.Android.Locale-Tests/Xamarin.Android.Locale-Tests.csproj
125+
126+
Would be run on Windows as:
127+
128+
> bin\Debug\bin\xabuild.exe /t:SignAndroidPackage tests\locales\Xamarin.Android.Locale-Tests\Xamarin.Android.Locale-Tests.csproj
129+
119130
# Unit Tests
120131

121132
The `xamarin-android` repo contains several unit tests:

Xamarin.Android.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ Project("{9344BDBB-3E7F-41FC-A0DD-8665D75EE146}") = "netstandard", "src\netstand
105105
EndProject
106106
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "setup-windows", "tools\setup-windows\setup-windows.csproj", "{73DF9E10-E933-4222-B8E1-F4536FFF9FAD}"
107107
EndProject
108+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "xabuild", "tools\xabuild\xabuild.csproj", "{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}"
109+
EndProject
108110
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.Android", "src\Mono.Android\Mono.Android.csproj", "{66CF299A-CE95-4131-BCD8-DB66E30C4BF7}"
109111
EndProject
110112
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.Android.Export", "src\Mono.Android.Export\Mono.Android.Export.csproj", "{B8105878-D423-4159-A3E7-028298281EC6}"
@@ -531,6 +533,18 @@ Global
531533
{1E5501E8-49C1-4659-838D-CC9720C5208F}.XAIntegrationDebug|Any CPU.Build.0 = Debug|Any CPU
532534
{1E5501E8-49C1-4659-838D-CC9720C5208F}.XAIntegrationRelease|Any CPU.ActiveCfg = Release|Any CPU
533535
{1E5501E8-49C1-4659-838D-CC9720C5208F}.XAIntegrationRelease|Any CPU.Build.0 = Release|Any CPU
536+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
537+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.Debug|AnyCPU.Build.0 = Debug|Any CPU
538+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.Release|AnyCPU.ActiveCfg = Release|Any CPU
539+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.Release|AnyCPU.Build.0 = Release|Any CPU
540+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.XAIntegrationDebug|AnyCPU.ActiveCfg = Debug|Any CPU
541+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.XAIntegrationDebug|AnyCPU.Build.0 = Debug|Any CPU
542+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.XAIntegrationRelease|AnyCPU.ActiveCfg = Debug|Any CPU
543+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.XAIntegrationRelease|AnyCPU.Build.0 = Debug|Any CPU
544+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.XAIntegrationDebug|Any CPU.ActiveCfg = Debug|Any CPU
545+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.XAIntegrationDebug|Any CPU.Build.0 = Debug|Any CPU
546+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.XAIntegrationRelease|Any CPU.ActiveCfg = Debug|Any CPU
547+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D}.XAIntegrationRelease|Any CPU.Build.0 = Debug|Any CPU
534548
EndGlobalSection
535549
GlobalSection(NestedProjects) = preSolution
536550
{8FF78EB6-6FC8-46A7-8A15-EBBA9045C5FA} = {E351F97D-EA4F-4E7F-AAA0-8EBB1F2A4A62}
@@ -585,6 +599,7 @@ Global
585599
{B8105878-D423-4159-A3E7-028298281EC6} = {04E3E11E-B47D-4599-8AFC-50515A95E715}
586600
{E34BCFA0-CAA4-412C-AA1C-75DB8D67D157} = {04E3E11E-B47D-4599-8AFC-50515A95E715}
587601
{1E5501E8-49C1-4659-838D-CC9720C5208F} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483}
602+
{B7A457E6-9CB6-43F6-BFD6-14D5397FB98D} = {864062D3-A415-4A6F-9324-5820237BA058}
588603
EndGlobalSection
589604
GlobalSection(MonoDevelopProperties) = preSolution
590605
Policies = $0

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/Builder.cs

Lines changed: 21 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ namespace Xamarin.ProjectTools
1010
{
1111
public class Builder : IDisposable
1212
{
13-
const string fixed_osx_xbuild_path = "/Library/Frameworks/Mono.framework/Commands";
14-
const string fixed_linux_xbuild_path = "/usr/bin";
15-
const string xbuildapp = "xbuild";
16-
const string msbuildapp = "msbuild";
17-
1813
public bool IsUnix { get; set; }
1914
public bool RunningMSBuild { get; set; }
2015
public LoggerVerbosity Verbosity { get; set; }
@@ -23,18 +18,6 @@ public class Builder : IDisposable
2318
public string BuildLogFile { get; set; }
2419
public bool ThrowOnBuildFailure { get; set; }
2520

26-
string GetUnixBuildExe ()
27-
{
28-
RunningMSBuild = false;
29-
var tooldir = Directory.Exists (fixed_osx_xbuild_path) ? fixed_osx_xbuild_path : fixed_linux_xbuild_path;
30-
string path = Path.Combine (tooldir, xbuildapp);
31-
if (!string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("USE_MSBUILD"))) {
32-
path = Path.Combine (tooldir, msbuildapp);
33-
RunningMSBuild = true;
34-
}
35-
return File.Exists (path) ? path : msbuildapp;
36-
}
37-
3821
string GetVisualStudio2017Directory ()
3922
{
4023
var editions = new [] {
@@ -54,38 +37,20 @@ string GetVisualStudio2017Directory ()
5437
return null;
5538
}
5639

57-
string GetWindowsBuildExe ()
58-
{
59-
RunningMSBuild = true;
60-
61-
//First try environment variable
62-
string msbuildExe = Environment.GetEnvironmentVariable ("XA_MSBUILD_EXE");
63-
if (!string.IsNullOrEmpty (msbuildExe) && File.Exists (msbuildExe))
64-
return msbuildExe;
65-
66-
//Next try VS 2017, MSBuild 15.0
67-
var visualStudioDirectory = GetVisualStudio2017Directory ();
68-
if (!string.IsNullOrEmpty(visualStudioDirectory)) {
69-
msbuildExe = Path.Combine (visualStudioDirectory, "MSBuild", "15.0", "Bin", "MSBuild.exe");
70-
71-
if (File.Exists (msbuildExe))
72-
return msbuildExe;
73-
}
74-
75-
//Try older than VS 2017, MSBuild 14.0
76-
msbuildExe = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ProgramFilesX86), "MSBuild", "14.0", "Bin", "MSBuild.exe");
77-
if (File.Exists (msbuildExe))
78-
return msbuildExe;
79-
80-
//MSBuild 4.0 last resort
81-
return Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.Windows), "Microsoft.NET", "Framework", "v4.0.30319", "MSBuild.exe");
82-
}
83-
84-
public string MSBuildExe {
40+
public string XABuildExe {
8541
get {
86-
return IsUnix
87-
? GetUnixBuildExe ()
88-
: GetWindowsBuildExe ();
42+
if (IsUnix) {
43+
if (!string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("USE_MSBUILD"))) {
44+
RunningMSBuild = true;
45+
}
46+
return Path.GetFullPath (Path.Combine (Root, "..", "..", "tools", "scripts", "xabuild"));
47+
}
48+
49+
#if DEBUG
50+
return Path.GetFullPath (Path.Combine (Root, "..", "..", "bin", "Debug", "bin", "xabuild.exe"));
51+
#else
52+
return Path.GetFullPath (Path.Combine (Root, "..", "..", "bin", "Release", "bin", "xabuild.exe"));
53+
#endif
8954
}
9055
}
9156

@@ -200,57 +165,22 @@ protected bool BuildInternal (string projectOrSolution, string target, string []
200165
buildLogFullPath, Verbosity.ToString ().ToLower ());
201166

202167
var start = DateTime.UtcNow;
203-
var homeDirectory = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
204-
var androidSdkToolPath = Path.Combine (homeDirectory, "android-toolchain");
205-
var sdkPath = Environment.GetEnvironmentVariable ("ANDROID_SDK_PATH");
206-
if (String.IsNullOrEmpty (sdkPath))
207-
sdkPath = GetPathFromRegistry ("AndroidSdkDirectory");
208-
if (String.IsNullOrEmpty (sdkPath))
209-
sdkPath = Path.GetFullPath (Path.Combine (androidSdkToolPath, "sdk"));
210-
var ndkPath = Environment.GetEnvironmentVariable ("ANDROID_NDK_PATH");
211-
if (String.IsNullOrEmpty (ndkPath))
212-
ndkPath = GetPathFromRegistry ("AndroidNdkDirectory");
213-
if (String.IsNullOrEmpty (ndkPath))
214-
ndkPath = Path.GetFullPath (Path.Combine (androidSdkToolPath, "ndk"));
215-
StringBuilder args = new StringBuilder ();
216-
var psi = new ProcessStartInfo (MSBuildExe);
217-
if (IsUnix) {
218-
if (Directory.Exists (sdkPath)) {
219-
args.AppendFormat ("/p:AndroidSdkDirectory=\"{0}\" ", sdkPath);
220-
}
221-
if (Directory.Exists (ndkPath)) {
222-
args.AppendFormat ("/p:AndroidNdkDirectory=\"{0}\" ", ndkPath);
223-
}
224-
var outdir = Path.GetFullPath (Path.Combine (FrameworkLibDirectory, "..", ".."));
225-
var targetsdir = Path.Combine (FrameworkLibDirectory, "xbuild");
226-
args.AppendFormat (" {0} ", logger);
168+
var args = new StringBuilder ();
169+
var psi = new ProcessStartInfo (XABuildExe);
170+
args.AppendFormat ("{0} /t:{1} {2} /p:UseHostCompilerIfAvailable=false /p:BuildingInsideVisualStudio=true",
171+
QuoteFileName(Path.Combine (Root, projectOrSolution)), target, logger);
227172

228-
if (Directory.Exists (targetsdir)) {
229-
psi.EnvironmentVariables ["TARGETS_DIR"] = targetsdir;
230-
psi.EnvironmentVariables ["MSBuildExtensionsPath"] = targetsdir;
231-
}
232-
if (Directory.Exists (outdir)) {
233-
var frameworksPath = Path.Combine (outdir, "lib", "xamarin.android", "xbuild-frameworks");
234-
psi.EnvironmentVariables ["MONO_ANDROID_PATH"] = outdir;
235-
args.AppendFormat ("/p:MonoDroidInstallDirectory=\"{0}\" ", outdir);
236-
psi.EnvironmentVariables ["XBUILD_FRAMEWORK_FOLDERS_PATH"] = frameworksPath;
237-
if (RunningMSBuild)
238-
args.AppendFormat ($"/p:TargetFrameworkRootPath={frameworksPath} ");
239-
}
240-
args.AppendFormat ("/t:{0} {1} /p:UseHostCompilerIfAvailable=false /p:BuildingInsideVisualStudio=true", target, QuoteFileName (Path.Combine (Root, projectOrSolution)));
241-
}
242-
else {
243-
args.AppendFormat ("{0} /t:{1} {2} /p:UseHostCompilerIfAvailable=false /p:BuildingInsideVisualStudio=true",
244-
QuoteFileName(Path.Combine (Root, projectOrSolution)), target, logger);
245-
}
246173
if (parameters != null) {
247174
foreach (var param in parameters) {
248175
args.AppendFormat (" /p:{0}", param);
249176
}
250177
}
178+
if (RunningMSBuild) {
179+
psi.EnvironmentVariables ["MSBUILD"] = "msbuild";
180+
}
251181
if (environmentVariables != null) {
252182
foreach (var kvp in environmentVariables) {
253-
psi.EnvironmentVariables[kvp.Key] = kvp.Value;
183+
psi.EnvironmentVariables [kvp.Key] = kvp.Value;
254184
}
255185
}
256186
psi.Arguments = args.ToString ();

tools/scripts/xabuild

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ if [ -z "$MSBUILD" ] ; then
4343
fi
4444

4545
if [ -z "$CONFIGURATION" ]; then
46-
for p in "$*"; do
46+
for p in "$@"; do
4747
case $p in
4848
/property:Configuration=*| \
4949
/p:Configuration=*| \
@@ -82,6 +82,11 @@ else
8282
exit 1
8383
fi
8484

85+
if [[ "$MSBUILD" == "msbuild" ]] ; then
86+
exec mono "$prefix/bin/xabuild.exe" "$@"
87+
exit $?
88+
fi
89+
8590
for t in "$TARGETS_DIR" "$prefix/lib/mono/xbuild" "$xa_prefix/xbuild" ; do
8691
if [ -z "$t" -o ! -d "$t" ]; then
8792
continue

tools/xabuild/App.config

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<configuration>
3+
<startup useLegacyV2RuntimeActivationPolicy="true">
4+
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
5+
</startup>
6+
<runtime>
7+
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
8+
<dependentAssembly>
9+
<assemblyIdentity name="Microsoft.Build.Framework" culture="neutral" publicKeyToken="b03f5f7f11d50a3a" />
10+
<bindingRedirect oldVersion="0.0.0.0-99.9.9.9" newVersion="15.1.0.0" />
11+
</dependentAssembly>
12+
<dependentAssembly>
13+
<assemblyIdentity name="Microsoft.Build" culture="neutral" publicKeyToken="b03f5f7f11d50a3a" />
14+
<bindingRedirect oldVersion="0.0.0.0-99.9.9.9" newVersion="15.1.0.0" />
15+
</dependentAssembly>
16+
<dependentAssembly>
17+
<assemblyIdentity name="Microsoft.Build.Tasks.Core" culture="neutral" publicKeyToken="b03f5f7f11d50a3a" />
18+
<bindingRedirect oldVersion="0.0.0.0-99.9.9.9" newVersion="15.1.0.0" />
19+
</dependentAssembly>
20+
<dependentAssembly>
21+
<assemblyIdentity name="Microsoft.Build.Utilities.Core" culture="neutral" publicKeyToken="b03f5f7f11d50a3a" />
22+
<bindingRedirect oldVersion="0.0.0.0-99.9.9.9" newVersion="15.1.0.0" />
23+
</dependentAssembly>
24+
</assemblyBinding>
25+
</runtime>
26+
</configuration>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
3+
4+
// Information about this assembly is defined by the following attributes.
5+
// Change them to the values specific to your project.
6+
7+
[assembly: AssemblyTitle ("xabuild")]
8+
[assembly: AssemblyDescription ("")]
9+
[assembly: AssemblyConfiguration ("")]
10+
[assembly: AssemblyCompany ("Microsoft Corporation")]
11+
[assembly: AssemblyProduct ("")]
12+
[assembly: AssemblyCopyright ("")]
13+
[assembly: AssemblyTrademark ("")]
14+
[assembly: AssemblyCulture ("")]
15+
16+
// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
17+
// The form "{Major}.{Minor}.*" will automatically update the build and revision,
18+
// and "{Major}.{Minor}.{Build}.*" will update just the revision.
19+
20+
[assembly: AssemblyVersion ("1.0.*")]
21+
22+
// The following attributes are used to specify the signing key for the assembly,
23+
// if desired. See the Mono documentation for more information about signing.
24+
25+
//[assembly: AssemblyDelaySign(false)]
26+
//[assembly: AssemblyKeyFile("")]

tools/xabuild/SymbolicLink.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.IO;
4+
using System.Runtime.InteropServices;
5+
using Mono.Unix;
6+
7+
namespace Xamarin.Android.Build
8+
{
9+
static class SymbolicLink
10+
{
11+
public static bool Create (string source, string target)
12+
{
13+
if (!Directory.Exists (source)) {
14+
if (Environment.OSVersion.Platform == PlatformID.Win32NT) {
15+
//NOTE: attempt with and without the AllowUnprivilegedCreate flag, seems to fix Windows Server 2016
16+
if (!CreateSymbolicLink (source, target, SymbolLinkFlag.Directory | SymbolLinkFlag.AllowUnprivilegedCreate) &&
17+
!CreateSymbolicLink (source, target, SymbolLinkFlag.Directory)) {
18+
var error = new Win32Exception ().Message;
19+
Console.Error.WriteLine ($"Unable to create symbolic link from `{source}` to `{target}`: {error}");
20+
return false;
21+
}
22+
} else {
23+
try {
24+
var fileInfo = new UnixFileInfo (target);
25+
fileInfo.CreateSymbolicLink (source);
26+
} catch (Exception exc) {
27+
Console.Error.WriteLine ($"Unable to create symbolic link from `{source}` to `{target}`: {exc.Message}");
28+
return false;
29+
}
30+
}
31+
}
32+
33+
return true;
34+
}
35+
36+
enum SymbolLinkFlag {
37+
File = 0,
38+
Directory = 1,
39+
AllowUnprivilegedCreate = 2,
40+
}
41+
42+
[DllImport ("kernel32.dll")]
43+
[return: MarshalAs (UnmanagedType.I1)]
44+
static extern bool CreateSymbolicLink (string lpSymlinkFileName, string lpTargetFileName, SymbolLinkFlag dwFlags);
45+
}
46+
}

0 commit comments

Comments
 (0)