Skip to content

Commit

Permalink
[macios] Add @(XcodeProjectReference)
Browse files Browse the repository at this point in the history
Introduces a new `@(XcodeProjectReference)` item to help simplify the
content that needs to be added to the binding .csproj file.  This item
will translate relevant metadata to the `@(NativeReference)` item that
is automatically added to the project after the xcode project is built.

Build task wrappers for `xcodebuild` and `sharpie` have been added to
improve msbuild output and error logging when a tool invocation fails.
  • Loading branch information
pjcollins committed May 28, 2024
1 parent b06188f commit 4f78bb6
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 162 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ FodyWeavers.xsd

xcuserdata/

build/
.build/
*.xcframework
Pods/
Expand Down
204 changes: 114 additions & 90 deletions eng/Common.macios.targets
Original file line number Diff line number Diff line change
@@ -1,94 +1,118 @@
<Project>

<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>

<XcodeDefaultBuildDir>.build</XcodeDefaultBuildDir>
<XcodeBuildDirName Condition=" '$(XcodeBuildDirName)' == '' ">$(XcodeDefaultBuildDir)</XcodeBuildDirName>

<_XcodeProjectFullPath>$([System.IO.Path]::GetFullPath($(XcodeProject)))</_XcodeProjectFullPath>

<XcodeScheme Condition=" '$(XcodeScheme)' == '' ">$([System.IO.Path]::GetFilenameWithoutExtension($(XcodeProject)))</XcodeScheme>

<XcodeProjectDir Condition=" '$(XcodeProjectDir)' == '' ">$([System.IO.Path]::GetDirectoryName($(_XcodeProjectFullPath)))</XcodeProjectDir>
<XcodeBuildDir Condition=" '$(XcodeBuildDir)' == '' ">$([System.IO.Path]::Combine($(XcodeProjectDir), $(XcodeBuildDirName)))</XcodeBuildDir>

<XcodeBuildXCFramework Condition=" '$(XcodeBuildXCFramework)' == '' ">True</XcodeBuildXCFramework>

<XcodeBuildiOS Condition=" '$(XcodeBuildiOS)' == '' ">True</XcodeBuildiOS>
<XcodeBuildiOSSimulator Condition=" '$(XcodeBuildiOSSimulator)' == '' ">True</XcodeBuildiOSSimulator>
<XcodeBuildMacCatalyst Condition=" '$(XcodeBuildMacCatalyst)' == '' ">True</XcodeBuildMacCatalyst>

<_XcodeBuildDirFullPath>$([System.IO.Path]::GetFullPath($(XcodeBuildDir)))</_XcodeBuildDirFullPath>
<_XcodeProjectDirFullPath>$([System.IO.Path]::GetFullPath($(XcodeProjectDir)))</_XcodeProjectDirFullPath>

<_XcArchiveiOSFullPath>$([System.IO.Path]::Combine($(_XcodeBuildDirFullPath), $(XcodeScheme)-ios.xcarchive))</_XcArchiveiOSFullPath>
<_XcArchiveiOSSimulatorFullPath>$([System.IO.Path]::Combine($(_XcodeBuildDirFullPath), $(XcodeScheme)-iossimulator.xcarchive))</_XcArchiveiOSSimulatorFullPath>
<_XcArchiveMacCatalystFullPath>$([System.IO.Path]::Combine($(_XcodeBuildDirFullPath), $(XcodeScheme)-maccatalyst.xcarchive))</_XcArchiveMacCatalystFullPath>
<_XcArchiveExtraArgs>ENABLE_BITCODE=NO SKIP_INSTALL=NO SWIFT_INSTALL_OBJC_HEADER=YES BUILD_LIBRARY_FOR_DISTRIBUTION=YES OTHER_LDFLAGS='-ObjC' OTHER_SWIFT_FLAGS='-no-verify-emitted-module-interface' OBJC_CFLAGS='-fno-objc-msgsend-selector-stubs -ObjC'</_XcArchiveExtraArgs>

<_XcFrameworkFullPath>$([System.IO.Path]::Combine($(_XcodeBuildDirFullPath), $(XcodeScheme).xcframework))</_XcFrameworkFullPath>
</PropertyGroup>

<ItemGroup>
<_XcodeProjectInputs Include="$(_XcodeProjectDirFullPath)/**/*.swift" Exclude="$(_XcodeBuildDirFullPath)/**" />
<_XcodeProjectInputs Include="$(_XcodeProjectDirFullPath)/**/*.h" Exclude="$(_XcodeBuildDirFullPath)/**" />
<_XcodeProjectInputs Include="$(_XcodeProjectFullPath)/*.pbxproj" />
<_XcodeProjectInputs Include="$(_XcodeProjectFullPath)/*.xcworkspace" />
</ItemGroup>

<PropertyGroup Condition="$(TargetFramework.Contains('ios')) Or $(TargetFramework.Contains('maccatalyst'))">
<_GenerateBindingsDependsOn>
BuildXCFramework;
ObjSharpieBind;
$(_GenerateBindingsDependsOn);
</_GenerateBindingsDependsOn>
</PropertyGroup>

<Target Name="BuildXCFramework"
Condition=" '$(XcodeBuildXCFramework)' == 'true' "
DependsOnTargets="$(BuildXCFrameworkDependsOnTargets)"
Inputs="@(_XcodeProjectInputs)"
Outputs="$(_XcFrameworkFullPath)/Info.plist">
<Error Condition=" !Exists('$(_XcodeProjectFullPath)') " Text="Xcode project '$(_XcodeProjectFullPath)' not found." />

<Exec Condition=" '$(XcodeBuildiOS)' == 'True' " Command="xcodebuild -project $(_XcodeProjectFullPath) archive -scheme $(XcodeScheme) -configuration $(Configuration) -archivePath $(_XcArchiveiOSFullPath) -destination 'generic/platform=iOS' $(_XcArchiveExtraArgs)" />
<Exec Condition=" '$(XcodeBuildiOSSimulator)' == 'True' " Command="xcodebuild -project $(_XcodeProjectFullPath) archive -scheme $(XcodeScheme) -configuration $(Configuration) -archivePath $(_XcArchiveiOSSimulatorFullPath) -destination 'generic/platform=iOS Simulator' $(_XcArchiveExtraArgs)" />
<Exec Condition=" '$(XcodeBuildMacCatalyst)' == 'True' " Command="xcodebuild -project $(_XcodeProjectFullPath) archive -scheme $(XcodeScheme) -configuration $(Configuration) -archivePath $(_XcArchiveMacCatalystFullPath) -destination 'generic/platform=macOS,variant=Mac Catalyst' $(_XcArchiveExtraArgs)" />

<ItemGroup>
<_CreateXcFxArgs Include="-create-xcframework" />
<_CreateXcFxArgs Condition=" '$(XcodeBuildiOS)' == 'True' " Include="-archive $(_XcArchiveiOSFullPath) -framework $(XcodeScheme).framework" />
<_CreateXcFxArgs Condition=" '$(XcodeBuildiOSSimulator)' == 'True' " Include="-archive $(_XcArchiveiOSSimulatorFullPath) -framework $(XcodeScheme).framework" />
<_CreateXcFxArgs Condition=" '$(XcodeBuildMacCatalyst)' == 'True' " Include="-archive $(_XcArchiveMacCatalystFullPath) -framework $(XcodeScheme).framework" />
<_CreateXcFxArgs Include="-output $(_XcFrameworkFullPath)" />
</ItemGroup>

<RemoveDir Directories="$(_XcFrameworkFullPath)" />
<Exec Command="xcodebuild @(_CreateXcFxArgs, ' ')" />
</Target>

<PropertyGroup>
<ObjSharpieBind Condition=" '$(ObjSharpieBind)' == '' ">True</ObjSharpieBind>
<ObjSharpieBindOutputDir Condition=" '$(ObjSharpieBindOutputDir)' == '' ">$(_XcodeBuildDirFullPath)/Binding</ObjSharpieBindOutputDir>
<ObjSharpieSourceHeader>$(_XcArchiveiOSFullPath)/Products/Library/Frameworks/$(XcodeScheme).framework/Headers/$(XcodeScheme)-Swift.h</ObjSharpieSourceHeader>
</PropertyGroup>

<ItemGroup>
<ObjcBindingApiDefinitionFiles Include="$(ObjSharpieBindOutputDir)/ApiDefinitions.cs" />
<_ObjSharpieInputs Include="$(ObjSharpieSourceHeader)" />
</ItemGroup>

<Target Name="ObjSharpieBind"
Condition="'$(ObjSharpieBind)' == 'true'"
Inputs="@(_ObjSharpieInputs)"
Outputs="@(ObjcBindingApiDefinitionFiles)">
<ItemGroup>
<_ObjSharpieArgs Include="--output=$(ObjSharpieBindOutputDir)" />
<_ObjSharpieArgs Include="--namespace=$(ObjSharpieBindNamespace)" />
<_ObjSharpieArgs Include="--framework $(_XcArchiveiOSFullPath)/Products/Library/Frameworks/$(XcodeScheme).framework" />
</ItemGroup>
<Exec Command="sharpie bind @(_ObjSharpieArgs, ' ')" />
</Target>
<Import Project="$(MSBuildThisFileDirectory)Common.targets" Condition=" '$(CommonTargetsImported)' != 'true' " />

<UsingTask TaskName="Sharpie" AssemblyFile="$(BindingExtBuildTasksAssembly)"/>
<UsingTask TaskName="XcodeBuild" AssemblyFile="$(BindingExtBuildTasksAssembly)"/>

<PropertyGroup>
<XcodeProjectConfiguration Condition=" '$(XcodeProjectConfiguration)' == '' ">Release</XcodeProjectConfiguration>
<XcodeBuildiOS Condition=" '$(XcodeBuildiOS)' == '' ">true</XcodeBuildiOS>
<XcodeBuildiOSSimulator Condition=" '$(XcodeBuildiOSSimulator)' == '' ">true</XcodeBuildiOSSimulator>
<XcodeBuildMacCatalyst Condition=" '$(XcodeBuildMacCatalyst)' == '' ">true</XcodeBuildMacCatalyst>
<EnableDefaultSharpieiOSItems Condition=" '$(EnableDefaultSharpieiOSItems)' == '' ">false</EnableDefaultSharpieiOSItems>

<_XcArchiveExtraArgs>$(_XcArchiveExtraArgs) ENABLE_BITCODE=NO SKIP_INSTALL=NO SWIFT_INSTALL_OBJC_HEADER=YES BUILD_LIBRARY_FOR_DISTRIBUTION=YES</_XcArchiveExtraArgs>
<_XcArchiveExtraArgs>$(_XcArchiveExtraArgs) OTHER_LDFLAGS=&quot;-ObjC&quot; OTHER_SWIFT_FLAGS=&quot;-no-verify-emitted-module-interface&quot; OBJC_CFLAGS=&quot;-fno-objc-msgsend-selector-stubs -ObjC&quot;</_XcArchiveExtraArgs>
</PropertyGroup>

<PropertyGroup Condition="$(TargetFramework.Contains('ios')) Or $(TargetFramework.Contains('maccatalyst'))">
<_GenerateBindingsDependsOn>
_BuildXcodeProjects;
_SharpieBindXcodeProjects;
$(_GenerateBindingsDependsOn);
</_GenerateBindingsDependsOn>
</PropertyGroup>

<ItemDefinitionGroup>
<XcodeProjectReference>
<Kind>Framework</Kind>
<SmartLink>true</SmartLink>
</XcodeProjectReference>
</ItemDefinitionGroup>

<!-- TODO Fix incremental builds -->
<Target Name="_GetBuildXcodeProjectsInputs">
<ItemGroup>
<_XcbInputs Include="@(XcodeProjectReference->'%(RootDir)%(Directory)**/*.swift')" />
<_XcbInputs Include="@(XcodeProjectReference->'%(RootDir)%(Directory)**/*.h')" />
<_XcbInputs Include="@(XcodeProjectReference->'%(RootDir)%(Directory)**/*.pbxproj')" />
<_XcbInputs Include="@(XcodeProjectReference->'%(RootDir)%(Directory)**/*.xcworkspace')"/>
<_XcbInputs Remove="@(XcodeProjectReference->'%(RootDir)%(Directory)%(SchemeName)/build/**/*')" />
</ItemGroup>
</Target>

<Target Name="_BuildXcodeProjects"
Condition=" '@(XcodeProjectReference->Count())' != '0' "
DependsOnTargets="_EnsureBuildTasksAssembly;_GetBuildXcodeProjectsInputs;$(BuildXcodeProjectsDependsOnTargets)"
Inputs="@(_XcbInputs)"
Outputs="@(XcodeProjectReference->'%(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName).xcframework/Info.plist')" >

<!-- Create xcarchive files for configured platforms -->
<XcodeBuild Condition=" '$(XcodeBuildiOS)' == 'true' "
Arguments="-project &quot;%(XcodeProjectReference.FullPath)&quot; archive -scheme %(SchemeName) -configuration $(XcodeProjectConfiguration) -archivePath &quot;@(XcodeProjectReference->'%(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName)-ios.xcarchive')&quot; -destination &quot;generic/platform=iOS&quot; $(_XcArchiveExtraArgs)"
WorkingDirectory="%(XcodeProjectReference.RootDir)%(XcodeProjectReference.Directory)" >
</XcodeBuild>

<XcodeBuild Condition=" '$(XcodeBuildiOSSimulator)' == 'true' "
Arguments="-project &quot;%(XcodeProjectReference.FullPath)&quot; archive -scheme %(SchemeName) -configuration $(XcodeProjectConfiguration) -archivePath &quot;@(XcodeProjectReference->'%(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName)-iossimulator.xcarchive')&quot; -destination &quot;generic/platform=iOS Simulator&quot; $(_XcArchiveExtraArgs)"
WorkingDirectory="%(XcodeProjectReference.RootDir)%(XcodeProjectReference.Directory)" >
</XcodeBuild>

<XcodeBuild Condition=" '$(XcodeBuildMacCatalyst)' == 'true' "
Arguments="-project &quot;%(XcodeProjectReference.FullPath)&quot; archive -scheme %(SchemeName) -configuration $(XcodeProjectConfiguration) -archivePath &quot;@(XcodeProjectReference->'%(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName)-maccatalyst.xcarchive')&quot; -destination &quot;generic/platform=macOS,variant=Mac Catalyst&quot; $(_XcArchiveExtraArgs)"
WorkingDirectory="%(XcodeProjectReference.RootDir)%(XcodeProjectReference.Directory)" >
</XcodeBuild>

<!-- Create xcframework file from xcarchive files -->
<ItemGroup>
<_CreateXcFxArgs Include="-create-xcframework" />
<_CreateXcFxArgs Condition=" '$(XcodeBuildiOS)' == 'true' " Include="@(XcodeProjectReference->'-archive %(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName)-ios.xcarchive')" />
<_CreateXcFxArgs Condition=" '$(XcodeBuildiOS)' == 'true' " Include="-framework %(XcodeProjectReference.SchemeName).framework" />
<_CreateXcFxArgs Condition=" '$(XcodeBuildiOSSimulator)' == 'true' " Include="@(XcodeProjectReference->'-archive %(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName)-iossimulator.xcarchive')" />
<_CreateXcFxArgs Condition=" '$(XcodeBuildiOSSimulator)' == 'true' " Include="-framework %(XcodeProjectReference.SchemeName).framework" />
<_CreateXcFxArgs Condition=" '$(XcodeBuildMacCatalyst)' == 'true' " Include="@(XcodeProjectReference->'-archive %(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName)-maccatalyst.xcarchive')" />
<_CreateXcFxArgs Condition=" '$(XcodeBuildMacCatalyst)' == 'true' " Include="-framework %(XcodeProjectReference.SchemeName).framework" />
<_CreateXcFxArgs Include="@(XcodeProjectReference->'-output %(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName).xcframework')" />
</ItemGroup>

<RemoveDir Directories="@(XcodeProjectReference->'%(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName).xcframework')" />

<XcodeBuild Arguments="@(_CreateXcFxArgs, ' ')"
WorkingDirectory="%(XcodeProjectReference.RootDir)%(XcodeProjectReference.Directory)" >
</XcodeBuild>

<ItemGroup>
<NativeReference Include="@(XcodeProjectReference->'%(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName).xcframework')">
<Kind>%(XcodeProjectReference.Kind)</Kind>
<SmartLink>%(XcodeProjectReference.SmartLink)</SmartLink>
</NativeReference>
</ItemGroup>

<Error Condition=" !Exists('@(NativeReference)') " Text="Xcode project built successfully but did not produce expected output file: '@(NativeReference)'" />
<Message Text="Adding reference to Xcode project output: @(NativeReference)" />
</Target>


<Target Name="_GetSharpieBindInputs">
<ItemGroup>
<_SharpieInputs Include="@(XcodeProjectReference->'%(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName)-ios.xcarchive/Products/Library/Frameworks/%(SchemeName).framework')" />
</ItemGroup>
</Target>

<Target Name="_SharpieBindXcodeProjects"
Condition=" '@(XcodeProjectReference->Count())' != '0' and '@(XcodeProjectReference->'%(SharpieBind)')' == 'true' "
DependsOnTargets="_GetSharpieBindInputs"
Inputs="@(_SharpieInputs)"
Outputs="@(XcodeProjectReference->'%(RootDir)%(Directory)%(SchemeName)/build/sharpie/ApiDefinitions.cs')">

<ItemGroup>
<_ObjSharpieArgs Include="@(XcodeProjectReference->'--output=%(RootDir)%(Directory)%(SchemeName)/build/sharpie')" />
<_ObjSharpieArgs Include="--namespace=%(XcodeProjectReference.SharpieNamespace)" />
<_ObjSharpieArgs Include="@(XcodeProjectReference->'--framework %(RootDir)%(Directory)%(SchemeName)/build/%(SchemeName)-ios.xcarchive/Products/Library/Frameworks/%(SchemeName).framework')" />
</ItemGroup>

<Sharpie Arguments="bind @(_ObjSharpieArgs, ' ')" />

Check failure on line 115 in eng/Common.macios.targets

View workflow job for this annotation

GitHub Actions / build

Parsing 1 header files...

Check failure on line 115 in eng/Common.macios.targets

View workflow job for this annotation

GitHub Actions / build

While building module 'MauiGoogleCast' imported from /private/var/folders/2f/8t5k6yr535sdw0s4glnpxrzm0000gn/T/com.xamarin.ObjectiveSharpie/a4e581b6fac34f69a78b49b3d11d8b2e.h:1:

Check failure on line 115 in eng/Common.macios.targets

View workflow job for this annotation

GitHub Actions / build

In file included from <module-includes>:2:

Check failure on line 115 in eng/Common.macios.targets

View workflow job for this annotation

GitHub Actions / build

/Users/runner/work/DotNet.Platform.SlimBindings/DotNet.Platform.SlimBindings/googlecast/macios/native/MauiGoogleCast/build/MauiGoogleCast-ios.xcarchive/Products/Library/Frameworks/MauiGoogleCast.framework/Headers/MauiGoogleCast-Swift.h:281:9: fatal error: module 'GoogleCast' not found

Check failure on line 115 in eng/Common.macios.targets

View workflow job for this annotation

GitHub Actions / build

281 | @import GoogleCast;

Check failure on line 115 in eng/Common.macios.targets

View workflow job for this annotation

GitHub Actions / build

| ~~~~~~~^~~~~~~~~~

Check failure on line 115 in eng/Common.macios.targets

View workflow job for this annotation

GitHub Actions / build

/private/var/folders/2f/8t5k6yr535sdw0s4glnpxrzm0000gn/T/com.xamarin.ObjectiveSharpie/a4e581b6fac34f69a78b49b3d11d8b2e.h:1:9: fatal error: could not build module 'MauiGoogleCast'

Check failure on line 115 in eng/Common.macios.targets

View workflow job for this annotation

GitHub Actions / build

1 | @import MauiGoogleCast;

Check failure on line 115 in eng/Common.macios.targets

View workflow job for this annotation

GitHub Actions / build

| ~~~~~~~^~~~~~~~~~~~~~

Check failure on line 115 in eng/Common.macios.targets

View workflow job for this annotation

GitHub Actions / build

Binding...
</Target>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,21 @@ public static void LogCodedError(this TaskLoggingHelper log, string code, string
message: message,
messageArgs: messageArgs);
}

public static void LogCodedWarning(this TaskLoggingHelper log, string code, string message, params object [] messageArgs)
{
log.LogWarning(
subcategory: string.Empty,
warningCode: code,
helpKeyword: string.Empty,
file: string.Empty,
lineNumber: 0,
columnNumber: 0,
endLineNumber: 0,
endColumnNumber: 0,
message: message,
messageArgs: messageArgs);
}

}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

Expand Down Expand Up @@ -31,7 +32,7 @@ public override bool Execute()
}
catch (Exception ex)
{
Log.LogCodedError($"{TaskPrefix}0001", ex.ToString());
Log.LogCodedError($"{TaskPrefix}0100", ex.ToString());
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using Microsoft.Build.Framework;
using System;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

using Microsoft.Build.Framework;

namespace Microsoft.Maui.BindingExtensions.Build.Tasks
{
public class Gradle : BindingToolTask
{
public override string TaskPrefix => "GDL";
public override string TaskPrefix => "GRDL";

protected override string ToolName => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "gradlew.bat" : "gradlew";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Runtime.InteropServices;

using Microsoft.Build.Framework;

namespace Microsoft.Maui.BindingExtensions.Build.Tasks
{
public class Sharpie : BindingToolTask
{
public override string TaskPrefix => "SHRP";

protected override string ToolName => "sharpie";


public string Arguments { get; set; } = string.Empty;


public Sharpie()
{
}

protected override string GenerateFullPathToTool()
{
return Path.Combine("/usr", "local", "bin", ToolExe);
}

protected override string GenerateCommandLineCommands() => Arguments;

public override bool RunTask()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
if (!File.Exists (GenerateFullPathToTool ())) {
Log.LogCodedWarning($"{TaskPrefix}1000", "Unable to locate `sharpie`, please install https://aka.ms/objective-sharpie.");
return false;
}

return base.RunTask();
}
else
{
Log.LogCodedWarning($"{TaskPrefix}1000", "sharpie is not currently supported on this platform. Please build this project on a macOS machine.");
return false;
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Runtime.InteropServices;

using Microsoft.Build.Framework;

namespace Microsoft.Maui.BindingExtensions.Build.Tasks
{
public class XcodeBuild : BindingToolTask
{
public override string TaskPrefix => "XCBD";

protected override string ToolName => "xcodebuild";


public string Arguments { get; set; } = string.Empty;


public XcodeBuild()
{
}

protected override string GenerateFullPathToTool()
{
return Path.Combine("/usr", "bin", ToolExe);
}

protected override string GenerateCommandLineCommands() => Arguments;

public override bool RunTask()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return base.RunTask();
}
else
{
Log.LogCodedWarning($"{TaskPrefix}1000", "xcodebuild is not currently supported on this platform. Please build this project on a macOS machine.");
return false;
}
}

}
}
Loading

0 comments on commit 4f78bb6

Please sign in to comment.