Skip to content

Commit

Permalink
[NativeAOT] Improving HelloiOS sample for NativeAOT to be testable wi…
Browse files Browse the repository at this point in the history
…th runtime packs (#86652)

- Enabled end-to-end testing of HelloiOS with NativeAOT runtime packs
- Update ILCompiler paths to handle PublishAotUsingRuntimePack=true
Fixes #80911
---------
Co-authored-by: Filip Navara <[email protected]>
  • Loading branch information
ivanpovazan authored Jun 8, 2023
1 parent 03f2be6 commit 2835110
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@
Text="Add a PackageReference for '$(_hostPackageName)' to allow cross-compilation for $(_targetArchitecture)" />

<!-- NativeAOT runtime pack assemblies need to be defined to avoid the default CoreCLR implementations being set as compiler inputs -->
<Error Condition="'@(PrivateSdkAssemblies)' == ''" Text="The PrivateSdkAssemblies ItemGroup is required for _ComputeAssembliesToCompileToNative" />
<Error Condition="'@(FrameworkAssemblies)' == ''" Text="The FrameworkAssemblies ItemGroup is required for _ComputeAssembliesToCompileToNative" />
<Error Condition="'@(PrivateSdkAssemblies)' == '' and '$(PublishAotUsingRuntimePack)' != 'true'" Text="The PrivateSdkAssemblies ItemGroup is required for _ComputeAssembliesToCompileToNative" />
<Error Condition="'@(FrameworkAssemblies)' == '' and '$(PublishAotUsingRuntimePack)' != 'true'" Text="The FrameworkAssemblies ItemGroup is required for _ComputeAssembliesToCompileToNative" />

<ComputeManagedAssembliesToCompileToNative
Assemblies="@(_ResolvedCopyLocalPublishAssets)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,26 +126,40 @@ The .NET Foundation licenses this file to you under the MIT license.

<!-- The properties below need to be defined only after we've found the correct runtime package reference -->
<Target Name="SetupProperties" DependsOnTargets="$(IlcSetupPropertiesDependsOn)" BeforeTargets="Publish">
<ItemGroup>
<_NETCoreAppFrameworkReference Include="@(ResolvedFrameworkReference)" Condition="'%(ResolvedFrameworkReference.RuntimePackName)' == 'Microsoft.NETCore.App.Runtime.NativeAOT.$(RuntimeIdentifier)'" />
</ItemGroup>

<PropertyGroup>
<!-- Define paths used in build targets to point to the runtime-specific ILCompiler implementation -->
<IlcToolsPath Condition="'$(IlcToolsPath)' == ''">$(IlcHostPackagePath)\tools\</IlcToolsPath>
<IlcSdkPath Condition="'$(IlcSdkPath)' == ''">$(RuntimePackagePath)\sdk\</IlcSdkPath>
<IlcFrameworkPath Condition="'$(IlcFrameworkPath)' == ''">$(RuntimePackagePath)\framework\</IlcFrameworkPath>
<IlcFrameworkNativePath Condition="'$(IlcFrameworkNativePath)' == ''">$(RuntimePackagePath)\framework\</IlcFrameworkNativePath>
<IlcFrameworkPath Condition="'$(IlcFrameworkPath)' == '' and '$(PublishAotUsingRuntimePack)' != 'true'">$(RuntimePackagePath)\framework\</IlcFrameworkPath>
<_NETCoreAppRuntimePackPath>%(_NETCoreAppFrameworkReference.RuntimePackPath)/runtimes/$(RuntimeIdentifier)/</_NETCoreAppRuntimePackPath>
<IlcFrameworkNativePath Condition="'$(IlcFrameworkNativePath)' == '' and '$(PublishAotUsingRuntimePack)' == 'true'">$(_NETCoreAppRuntimePackPath)\native\</IlcFrameworkNativePath>
<IlcFrameworkNativePath Condition="'$(IlcFrameworkNativePath)' == '' and '$(PublishAotUsingRuntimePack)' != 'true'">$(RuntimePackagePath)\framework\</IlcFrameworkNativePath>
<IlcSdkPath Condition="'$(IlcSdkPath)' == '' and '$(PublishAotUsingRuntimePack)' == 'true'">$(IlcFrameworkNativePath)</IlcSdkPath>
<IlcSdkPath Condition="'$(IlcSdkPath)' == '' and '$(PublishAotUsingRuntimePack)' != 'true'">$(RuntimePackagePath)\sdk\</IlcSdkPath>
<IlcMibcPath Condition="'$(IlcMibcPath)' == ''">$(RuntimePackagePath)\mibc\</IlcMibcPath>
</PropertyGroup>

<ItemGroup>
<PrivateSdkAssemblies Include="$(IlcSdkPath)*.dll" />
<ItemGroup Condition="'$(PublishAotUsingRuntimePack)' == 'true'">
<PrivateSdkAssemblies Include="$(IlcSdkPath)*.dll"/>
<FrameworkAssemblies Include="@(RuntimePackAsset)" Condition="'%(Extension)' == '.dll'" />
<DefaultFrameworkAssemblies Include="@(FrameworkAssemblies)" />
</ItemGroup>

<ItemGroup Condition="'$(PublishAotUsingRuntimePack)' != 'true'">
<PrivateSdkAssemblies Include="$(IlcSdkPath)*.dll"/>
<!-- Exclude unmanaged dlls -->
<FrameworkAssemblies Include="$(IlcFrameworkPath)*.dll" Exclude="$(IlcFrameworkPath)*.Native.dll;$(IlcFrameworkPath)msquic.dll" />

<MibcFile Include="$(IlcMibcPath)*.mibc" Condition="'$(IlcPgoOptimize)' == 'true'" />

<DefaultFrameworkAssemblies Include="@(FrameworkAssemblies)" />
<DefaultFrameworkAssemblies Include="@(PrivateSdkAssemblies)" />
</ItemGroup>

<ItemGroup>
<MibcFile Include="$(IlcMibcPath)*.mibc" Condition="'$(IlcPgoOptimize)' == 'true'" />
</ItemGroup>
</Target>

<Target Name="ComputeIlcCompileInputs" DependsOnTargets="$(IlcDynamicBuildPropertyDependencies)" BeforeTargets="Publish">
Expand Down
2 changes: 1 addition & 1 deletion src/installer/pkg/projects/nativeaot-packages.proj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<ItemGroup>
<Project Include="Microsoft.DotNet.ILCompiler\Microsoft.DotNet.ILCompiler.pkgproj" />
<ProjectReference Include="@(Project)" />
<ProjectReference Condition="'$(BuildNativeAOTRuntimePack)' != 'true'" Include="@(Project)" />
<ProjectReference Include="@(Project)" AdditionalProperties="PackageTargetRuntime=$(OutputRID)" />
</ItemGroup>

Expand Down
26 changes: 22 additions & 4 deletions src/mono/sample/iOS-NativeAOT/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,41 @@ TARGET_ARCH?=$(shell . $(TOP)eng/native/init-os-and-arch.sh && echo $${arch})
TARGET_OS?=iossimulator
DEPLOY_AND_RUN?=false
STRIP_DEBUG_SYMBOLS?=false
USE_RUNTIME_PACKS?=false

REPO_DIR=$(realpath $(TOP))
TASKS_DIR=$(REPO_DIR)/src/tasks
DOTNET=$(REPO_DIR)/dotnet.sh
BUILD_SCRIPT=$(REPO_DIR)/build.sh
ifeq ($(USE_RUNTIME_PACKS),false)
LOCAL_NUGET_RESTORE_DIR=
else
LOCAL_NUGET_RESTORE_DIR=$(realpath .)/packages
endif

world: build-deps all

# build all dependencies: runtime, nativeAot and all the libs
ifeq ($(USE_RUNTIME_PACKS),false)
build-deps: build-runtime-ilc build-libs-all
else
build-deps: build-runtime-ilc-packs build-libs-all-packs
endif

# building for host
build-runtime-ilc:
$(BUILD_SCRIPT) clr+clr.aot -c $(BUILD_CONFIG)

build-ilc:
$(BUILD_SCRIPT) clr.aot -c $(BUILD_CONFIG)
build-runtime-ilc-packs:
$(BUILD_SCRIPT) clr+clr.aot+libs+packs -c $(BUILD_CONFIG)

# building for target platform
build-libs-all:
$(BUILD_SCRIPT) clr.nativeaotruntime+clr.nativeaotlibs+libs -c $(BUILD_CONFIG) -os $(TARGET_OS) -arch $(TARGET_ARCH)

build-libs-all-packs:
$(BUILD_SCRIPT) clr.nativeaotruntime+clr.nativeaotlibs+libs+packs -c $(BUILD_CONFIG) -os $(TARGET_OS) -arch $(TARGET_ARCH) /p:BuildNativeAOTRuntimePack=true

build-libs-nativeaot:
$(BUILD_SCRIPT) clr.nativeaotruntime+clr.nativeaotlibs -c $(BUILD_CONFIG) -os $(TARGET_OS) -arch $(TARGET_ARCH)

Expand All @@ -37,14 +50,19 @@ all: appbuilder hello-app
appbuilder:
$(DOTNET) build -c $(BUILD_CONFIG) $(TASKS_DIR)/AppleAppBuilder/AppleAppBuilder.csproj

ifeq ($(USE_RUNTIME_PACKS),true)
hello-app: export NUGET_PACKAGES = $(LOCAL_NUGET_RESTORE_DIR)
endif

hello-app: clean
$(DOTNET) \
build -c $(BUILD_CONFIG) \
publish -c $(BUILD_CONFIG) \
-p:TargetOS=$(TARGET_OS) \
-p:TargetArchitecture=$(TARGET_ARCH) \
-p:DeployAndRun=$(DEPLOY_AND_RUN) \
-p:StripDebugSymbols=$(STRIP_DEBUG_SYMBOLS) \
-p:PublishAotUsingRuntimePack=$(USE_RUNTIME_PACKS) \
-bl

clean:
rm -rf obj bin
rm -rf obj bin *.binlog $(LOCAL_NUGET_RESTORE_DIR)
62 changes: 40 additions & 22 deletions src/mono/sample/iOS-NativeAOT/Program.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,48 @@
<OutputPath>bin</OutputPath>
<IntermediateOutputPath>$(MSBuildThisFileDirectory)/obj/</IntermediateOutputPath>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
<TargetOS Condition="'$(TargetOS)' == ''">ios</TargetOS>
<TargetOS Condition="'$(TargetsiOSSimulator)' == 'true'">iossimulator</TargetOS>
<TargetOS Condition="'$(TargetOS)' == ''">iossimulator</TargetOS>
<DeployAndRun Condition="'$(DeployAndRun)' == ''">true</DeployAndRun>
<RuntimeIdentifier>$(TargetOS)-$(TargetArchitecture)</RuntimeIdentifier>
<AppName>HelloiOS</AppName>
<StripDebugSymbols Condition="'$(StripDebugSymbols)' == ''">false</StripDebugSymbols>
<!-- NativeAOT-specific props -->
<NativeLib>static</NativeLib>
<CustomNativeMain>true</CustomNativeMain>
<UseNativeAOTRuntime Condition="'$(UseNativeAOTRuntime)' == ''">true</UseNativeAOTRuntime>
<!-- FIXME: Once we support building System.Globalization.Native and icu, should be removed -->
<InvariantGlobalization>true</InvariantGlobalization>
<!-- FIXME: We do not use publish targets yet, but we need to create a publish directory -->
<PublishDir Condition="'$(PublishDir)' == ''">$(OutputPath)/publish</PublishDir>
<NativeLib>static</NativeLib>
<CustomNativeMain>true</CustomNativeMain>
<!-- Escape NativeAOT bundling targets -->
<NativeCompilationDuringPublish>false</NativeCompilationDuringPublish>
<IlcCompileDependsOn>Compile;ComputeIlcCompileInputs;SetupOSSpecificProps;PrepareForILLink</IlcCompileDependsOn>
</PropertyGroup>

<PropertyGroup Condition="'$(PublishAotUsingRuntimePack)' == 'true'">
<PublishAot>true</PublishAot>
<MSBuildWarningsAsErrors>$(MSBuildWarningsAsErrors);NU1603</MSBuildWarningsAsErrors>
<UseLocalTargetingRuntimePack>false</UseLocalTargetingRuntimePack>
<RestoreAdditionalProjectSources>$([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'packages', '$(CoreCLRConfiguration)', 'Shipping'))</RestoreAdditionalProjectSources>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\iOS\Program.cs" Link="Program.cs" />
<DirectPInvoke Include="__Internal" />
</ItemGroup>

<PropertyGroup Condition="'$(TargetOS)' == 'maccatalyst'">
<DevTeamProvisioning Condition="'$(DevTeamProvisioning)' == ''">adhoc</DevTeamProvisioning>
<EnableAppSandbox Condition="'$(EnableAppSandbox)' == ''">false</EnableAppSandbox>
</PropertyGroup>
<!-- Fix temporary regression, will be fixed on the main in a separate PR: https://github.com/dotnet/runtime/issues/80911 -->
<Import Project="$(CoreClrProjectRoot)nativeaot\BuildIntegration\Microsoft.DotNet.ILCompiler.SingleEntry.targets" />
<Import Condition="'$(PublishAotUsingRuntimePack)' != 'true'" Project="$(CoreClrProjectRoot)nativeaot\BuildIntegration\Microsoft.DotNet.ILCompiler.SingleEntry.targets" />
<UsingTask TaskName="AppleAppBuilderTask"
AssemblyFile="$(AppleAppBuilderTasksAssemblyPath)" />

<!-- FIXME: Once we set up builing appropriate runtime package for iOS-like platforms the following properties should be removed -->
<Target Name="ConfigureIlcPathsForiOSCrossCompilation">
<!-- Use locally built runtime and ilcompiler packages -->
<Target Name="UpdateRuntimePackVersion" Condition="'$(PublishAotUsingRuntimePack)' == 'true'" BeforeTargets="ProcessFrameworkReferences">
<ItemGroup>
<KnownRuntimePack Condition="%(RuntimePackLabels) == 'NativeAOT'" LatestRuntimeFrameworkVersion="8.0.0-dev" />
<KnownILCompilerPack Update="Microsoft.DotNet.ILCompiler">
<ILCompilerPackVersion>8.0.0-dev</ILCompilerPackVersion>
</KnownILCompilerPack>
</ItemGroup>
</Target>

<!-- Run before ILC setup -->
<Target Name="ConfigureIlcPathsForiOSCrossCompilation" Condition="'$(PublishAotUsingRuntimePack)' != 'true'"
BeforeTargets="SetupProperties">
<PropertyGroup>
<IlcPath>$([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'bin', 'coreclr', '$(HostOS).$(BuildArchitecture).$(CoreCLRConfiguration)', 'ilc'))</IlcPath>
<IlcToolsPath>$(IlcPath)</IlcToolsPath>
Expand All @@ -47,23 +57,29 @@

<Target Name="BuildAppBundle"
AfterTargets="Build"
DependsOnTargets="ConfigureIlcPathsForiOSCrossCompilation;SetupProperties;ComputeIlcCompileInputs;IlcCompile">
DependsOnTargets="IlcCompile;CopyFilesToPublishDirectory">

<PropertyGroup>
<AppDir>$(MSBuildThisFileDirectory)$(PublishDir)\app</AppDir>
<IosSimulator Condition="'$(TargetsiOSSimulator)' == 'true'">iPhone 11</IosSimulator>
<Optimized Condition="'$(Configuration)' == 'Release'">True</Optimized>
<StripDebugSymbols Condition="'$(StripDebugSymbols)' == ''">false</StripDebugSymbols>
<DevTeamProvisioning Condition="'$(TargetOS)' == 'maccatalyst' and '$(DevTeamProvisioning)' == ''">adhoc</DevTeamProvisioning>
<EnableAppSandbox Condition="'$(TargetOS)' == 'maccatalyst' and '$(EnableAppSandbox)' == ''">false</EnableAppSandbox>
</PropertyGroup>

<ItemGroup>
<NativeLibrary Include="%(ManagedBinary.IlcOutputFile)" />
<_LinkerFlagsToDrop Include="@(NativeFramework->'-framework %(Identity)')" />
<_LinkerFlagsToDrop Include="@(LinkerArg)" Condition="$([System.String]::new('%(Identity)').Contains('swift'))" />
<LinkerArg Remove="@(_LinkerFlagsToDrop)" />
<ExtraLinkerArguments Include="@(LinkerArg)" />
<_NativeDependencies Include="%(ManagedBinary.IlcOutputFile)" />
</ItemGroup>

<RemoveDir Directories="$(AppDir)" />

<AppleAppBuilderTask
UseNativeAOTRuntime="$(UseNativeAOTRuntime)"
NativeDependencies="@(NativeLibrary)"
NativeDependencies="@(_NativeDependencies)"
TargetOS="$(TargetOS)"
Arch="$(TargetArchitecture)"
ProjectName="$(AppName)"
Expand All @@ -74,8 +90,10 @@
OutputDirectory="$(AppDir)"
Optimized="$(Optimized)"
InvariantGlobalization="$(InvariantGlobalization)"
EnableAppSandbox="$(EnableAppSandbox)"
StripSymbolTable="$(StripDebugSymbols)"
AppDir="$(MSBuildThisFileDirectory)$(PublishDir)" >
AppDir="$(MSBuildThisFileDirectory)$(PublishDir)"
ExtraLinkerArguments="@(ExtraLinkerArguments)" >
<Output TaskParameter="AppBundlePath" PropertyName="AppBundlePath" />
<Output TaskParameter="XcodeProjectPath" PropertyName="XcodeProjectPath" />
</AppleAppBuilderTask>
Expand Down
61 changes: 51 additions & 10 deletions src/mono/sample/iOS-NativeAOT/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,61 @@ This sample application is intended to be used by developers who work on enablin

The sample shares the source code with the Mono sample specified at: `../iOS/Program.cs` and in general should have the same behavior as MonoAOT.

## Limitations
## Scenarios

The application is **_currently_** relying on the following:
1. Internal dependencies - locally building the internals is required as runtime and tools nuget packages are still not being produced
2. Invariant globalization - `System.Globalization.Native` is currently not being built as part of NativeAOT framework for iOS-like platforms
3. No publish targets - the SDK and MSBuild integration is still not complete
There are two scenarios for building and testing the NativeAOT support for iOS:
1. Testing against locally built internals
- Uses locally built ILCompiler, build integration targets, framework and runtime libraries
- Should be used in CI testing
2. Testing against locally built packages - **end-to-end** testing
- References locally built ILCompiler and runtime packages
- Should be used by developers performing end-to-end validation

## How to build and test

### Building for the first time
### 1. Testing against locally built internals

When building for the first time (on a clean checkout) run from this directory the following `make` command:
``` bash
make world
```
This will first build all required runtime components and dependencies, after which it will build the sample app and bundle it into an application bundle.

The command performs the following:
1. Build all required runtime components and dependencies
2. Build the sample app and bundle it into an application bundle

By default the build will use `Debug` build configuration and target `iossimulator`.
To change this behavior, specify the desired setting in the following way:
``` bash
make world BUILD_CONFIG=Release TARGET_OS=ios
```

### 2. Testing against locally built packages - end-to-end testing

When building for the first time (on a clean checkout) run from this directory the following `make` command:
``` bash
make world USE_RUNTIME_PACKS=true
```

The command performs the following:
1. Builds ILCompiler and runtime packages:
- `Microsoft.DotNet.ILCompiler.8.0.0-dev` (host)
- `runtime.<host_os>-<host_arch>.Microsoft.DotNet.ILCompiler.8.0.0-dev` (host)
- `Microsoft.NETCore.App.Runtime.NativeAOT.<target_os>-<target_arch>.8.0.0-dev` (target)

NOTE:
- The packages can be found at: `artifacts/packages/<config>/Shipping/*.8.0.0-dev.nupkg`
- During the build of the application NuGet is instructed to use the local `./packages` folder (in the current directory) as the folder for restored packages, which should be deleted when testing incremental changes. This is required as the packages built locally always have the same version - `8.0.0-dev`. For convenience, targets in the `Makefile` are automatically removing this folder on a rebuild.
2. Build the sample app using locally built packages 1) and bundle it into an application bundle

By default the build will use `Debug` build configuration and target `iossimulator`.
To change this behavior, specify the desired setting in the following way:
``` bash
make world USE_RUNTIME_PACKS=true BUILD_CONFIG=Release TARGET_OS=ios
```

NOTE: In general, the make variable `USE_RUNTIME_PACKS` controls which scenario will be used during the build (the default value is `false`)

### To avoid building all the dependencies

For future builds, you can run just:
Expand All @@ -46,6 +79,8 @@ For convenience, it is also possible to rebuild only the application it self wit
make hello-app
```

NOTE: Pay attention to the scenario you are testing `USE_RUNTIME_PACKS=true or false`

### Deploy and run

#### Simulator
Expand All @@ -64,16 +99,22 @@ export DevTeamProvisioning=A1B2C3D4E5; make hello-app TARGET_OS=ios DEPLOY_AND_R
```
Assuming `A1B2C3D4E5` is a valid team ID.

#### One-liner
### One-liners

On a clean dotnet/runtime checkout, from this directory, run:

``` bash
export DevTeamProvisioning=A1B2C3D4E5; make world BUILD_CONFIG=Release TARGET_OS=ios DEPLOY_AND_RUN=true
```

This command will build everything necessary to run and deploy the application on an iOS device.
- This command will build everything necessary to run and deploy the application on an iOS device using the locally built internals.

``` bash
export DevTeamProvisioning=A1B2C3D4E5; make world BUILD_CONFIG=Release TARGET_OS=ios DEPLOY_AND_RUN=true USE_RUNTIME_PACKS=true
```

- This command will build everything necessary to run and deploy the application on an iOS device using the locally built packages.

### Custom builds

Check the `Makefile` for individual list of targets and variables to customize the build.
Check the `Makefile` for individual list of targets and variables to further customize your builds.
13 changes: 5 additions & 8 deletions src/tasks/AppleAppBuilder/Templates/CMakeLists.txt.template
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,11 @@ target_link_libraries(
"-lc++"
"-liconv"
%NativeLibrariesToLink%
%APP_LINKER_ARGS%
%APP_LINK_LIBRARIES%
)

if(%UseNativeAOTRuntime%)
target_link_libraries(
%ProjectName%
PRIVATE
"-Wl,-u,_NativeAOT_StaticInitialization"
"-Wl,-segprot,__THUNKS,rx,rx"
set_target_properties(
%ProjectName%
PROPERTIES LINK_FLAGS
%EXTRA_LINKER_ARGS%
)
endif()
12 changes: 4 additions & 8 deletions src/tasks/AppleAppBuilder/Xcode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -411,16 +411,12 @@ public string GenerateCMake(
frameworks = "\"-framework GSS\"";
}

string appLinkerArgs = "";
foreach(string linkerArg in extraLinkerArgs)
{
appLinkerArgs += $" \"{linkerArg}\"{Environment.NewLine}";
}

appLinkerArgs += $" {frameworks}{Environment.NewLine}";
string appLinkLibraries = $" {frameworks}{Environment.NewLine}";
string extraLinkerArgsConcat = $"\"{string.Join('\n', extraLinkerArgs)}\"";

cmakeLists = cmakeLists.Replace("%NativeLibrariesToLink%", toLink);
cmakeLists = cmakeLists.Replace("%APP_LINKER_ARGS%", appLinkerArgs);
cmakeLists = cmakeLists.Replace("%APP_LINK_LIBRARIES%", appLinkLibraries);
cmakeLists = cmakeLists.Replace("%EXTRA_LINKER_ARGS%", extraLinkerArgsConcat);
cmakeLists = cmakeLists.Replace("%AotSources%", aotSources);
cmakeLists = cmakeLists.Replace("%AotTargetsList%", aotList);
cmakeLists = cmakeLists.Replace("%AotModulesSource%", string.IsNullOrEmpty(aotSources) ? "" : "modules.m");
Expand Down

0 comments on commit 2835110

Please sign in to comment.