Skip to content
Permalink
Browse files

[Xamarin.Android.Build.Tasks] Use @(AndroidDefineConstants) (#635)

Fixes: https://bugzilla.xamarin.com/show_bug.cgi?id=56976

A "funny" thing happened in the migration from `xbuild` to `msbuild`:
F# compilation behavior changed.

This makes nearly no sense at all; it's the confluence of:

 1. `fsc.exe` deosn't support specifying multiple symbols in a single
    `fsc.exe --define`. Specifically, *unlike* `csc`/`mcs`, the
    following command line does *not* define the symbols `A` and `B`;
    it is instead, effectively, *ignored*:

        fsc.exe "--define=A;B"

 2. The `<GetAndroidDefineConstants/>` task generated a `;`-separated
    string value.

 3. `xbuild` differs from `msbuild` in `;`-splitting behavior.

The `<GetAndroidDefineConstants/>` task is used to determine
additional conditional compilation symbols based on the API level. For
example, if an app sets `$(TargetFrameworkVersion)`=v2.3 (API-10),
then `<GetAndroidDefineConstants/>` will create the MSBuild property
`$(AndroidDefineConstants)` with the value:

	__XAMARIN_ANDROID_v1_0__;__MOBILE__;__ANDROID__;__ANDROID_1__;__ANDROID_2__;__ANDROID_3__;__ANDROID_4__;__ANDROID_5__;__ANDROID_6__;__ANDROID_7__;__ANDROID_8__;__ANDROID_9__;__ANDROID_10__

This is provided as a courtesy to developers so that they can easily
conditionally include or exclude code based on the
`$(TargetFrameworkVersion) value.

When using `xbuild`, this works: the `$(AndroidDefineConstants)`
property is split on the `;` to create an `ITaskItem[]`, which is
provided to the `<Fsc/>` tasks' `Fsc.DefineConstants` property. This
in turn allows the `<Fsc/>` task to generate a sequence of `--define`
options, one per symbol:

	fsc.exe --define=__XAMARIN_ANDROID_v1_0__ --define=__MOBILE__ --define=__ANDROID__ ...

Unfortunately, this behavior is an `xbuild` bug, in that `msbuild`
*doesn't* behave this way. Instead, `msbuild` doesn't split the
`$(AndroidDefineConstants)` property on `;` to create an
`ITaskItem[]`; instead, it creates a single element which contains the
`$(AndroidDefineConstants)` value as-is. This causes the `<Fsc/>` task
to generate a command-line like:

	fsc.exe "--define=__XAMARIN_ANDROID_v1_0__;__MOBILE__;__ANDROID__;..." ...

If this were `csc`, that would be *fine*, but it's not `csc`, and it's
not fine.

The result is that code that requires that e.g. `__ANDROID__` be
defined no longer works as expected, becuase `__ANDROID__` *isn't*
defined when building with `msbuild`.

The fix? If we need an `ITaskItem[]`, we should create one, not create
a string and assume that `msbuild` will split on `;`. Change the
`GetAndroidDefineConstants.AndroidDefineConstants` property from being
a `string` into an `ITaskItem[]`, where each element of the array is a
separate symbol which should be defined. This allows the desired
values to be sent to the `Fsc.DefineConstants` property, allowing
compilation to work as desired.
  • Loading branch information...
jonpryor authored and dellis1972 committed Jun 7, 2017
1 parent 8467ad6 commit 669d51536bc356a9b52bca655fb3338939fcb34f
@@ -1,6 +1,7 @@
// Copyright (C) 2011 Xamarin, Inc. All rights reserved.

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
@@ -16,21 +17,25 @@ public class GetAndroidDefineConstants : Task
public string ProductVersion { get; set; }

[Output]
public string AndroidDefineConstants { get; set; }
public ITaskItem[] AndroidDefineConstants { get; set; }

public override bool Execute ()
{
var sb = new StringBuilder ();
var constants = new List<ITaskItem> ();

if (!string.IsNullOrEmpty (ProductVersion)) {
sb.AppendFormat ("__XAMARIN_ANDROID_{0}__;", Regex.Replace (ProductVersion, "[^A-Za-z0-9]", "_"));
var version = Regex.Replace (ProductVersion, "[^A-Za-z0-9]", "_");
constants.Add (new TaskItem ($"__XAMARIN_ANDROID_{version}__"));
}
sb.Append ("__MOBILE__;__ANDROID__");

for (int i = 1; i <= AndroidApiLevel; ++i)
sb.Append (";__ANDROID_").Append (i).Append ("__");
constants.Add (new TaskItem ("__MOBILE__"));
constants.Add (new TaskItem ("__ANDROID__"));

AndroidDefineConstants = sb.ToString ();
for (int i = 1; i <= AndroidApiLevel; ++i) {
constants.Add (new TaskItem ($"__ANDROID_{i}__"));
}

AndroidDefineConstants = constants.ToArray ();

return true;
}
@@ -50,6 +50,29 @@ public void BuildBasicApplicationReleaseFSharp ()
}
}

[Test]
public void FSharpAppHasAndroidDefine ()
{
var proj = new XamarinAndroidApplicationProject () {
Language = XamarinAndroidProjectLanguage.FSharp,
};
proj.Sources.Add (new BuildItem ("Compile", "IsAndroidDefined.fs") {
TextContent = () => @"
module Xamarin.Android.Tests
// conditional compilation; can we elicit a compile-time error?
let x =
#if __ANDROID__
42
#endif // __ANDROID__
printf ""%d"" x
",
});
using (var b = CreateApkBuilder ("temp/" + nameof (FSharpAppHasAndroidDefine))) {
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
}
}

[Test]
public void BuildApplicationAndClean ([Values (false, true)] bool isRelease)
{
@@ -822,12 +822,12 @@ because xbuild doesn't support framework reference assemblies.

<!-- Get the defined constants for this API Level -->
<GetAndroidDefineConstants AndroidApiLevel="$(_SupportedApiLevel)" ProductVersion="$(MonoAndroidVersion)">
<Output TaskParameter="AndroidDefineConstants" PropertyName="AndroidDefineConstants" />
<Output TaskParameter="AndroidDefineConstants" ItemName="AndroidDefineConstants" />
</GetAndroidDefineConstants>

<CreateProperty Value="$(DefineConstants);$(AndroidDefineConstants)">
<Output TaskParameter="Value" PropertyName="DefineConstants" />
</CreateProperty>
<PropertyGroup>
<DefineConstants>$(DefineConstants);@(AndroidDefineConstants)</DefineConstants>
</PropertyGroup>
</Target>

<Target Name="AndroidPrepareForBuild" DependsOnTargets="$(_OnResolveMonoAndroidSdks)" />

0 comments on commit 669d515

Please sign in to comment.
You can’t perform that action at this time.