Skip to content

Commit 4be3dce

Browse files
authored
dotnet-interactive assembly extension (#517)
1 parent ce23177 commit 4be3dce

File tree

15 files changed

+551
-14
lines changed

15 files changed

+551
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
<RootNamespace>Microsoft.Spark.Extensions.DotNet.Interactive.UnitTest</RootNamespace>
6+
<IsPackable>false</IsPackable>
7+
8+
<RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet3.1/nuget/v3/index.json</RestoreAdditionalProjectSources>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Moq" Version="4.10.0" />
13+
<PackageReference Include="Microsoft.DotNet.Interactive" Version="1.0.0-beta.20262.1" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\Microsoft.Spark.Extensions.DotNet.Interactive\Microsoft.Spark.Extensions.DotNet.Interactive.csproj" />
18+
<ProjectReference Include="..\..\Microsoft.Spark\Microsoft.Spark.csproj" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<Compile Include="..\..\Microsoft.Spark.UnitTest\TestUtils\TemporaryDirectory.cs" />
23+
</ItemGroup>
24+
25+
</Project>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using Microsoft.DotNet.Interactive.Utility;
9+
using Microsoft.Spark.UnitTest.TestUtils;
10+
using Microsoft.Spark.Utils;
11+
using Moq;
12+
using Xunit;
13+
14+
namespace Microsoft.Spark.Extensions.DotNet.Interactive.UnitTest
15+
{
16+
public class PackageResolverTests
17+
{
18+
[Fact]
19+
public void TestPackageResolver()
20+
{
21+
using var tempDir = new TemporaryDirectory();
22+
23+
string packageName = "package.name";
24+
string packageVersion = "0.1.0";
25+
string packageRootPath =
26+
Path.Combine(tempDir.Path, "path", "to", "packages", packageName, packageVersion);
27+
string packageFrameworkPath = Path.Combine(packageRootPath, "lib", "framework");
28+
29+
Directory.CreateDirectory(packageRootPath);
30+
var nugetFile = new FileInfo(
31+
Path.Combine(packageRootPath, $"{packageName}.{packageVersion}.nupkg"));
32+
using (File.Create(nugetFile.FullName))
33+
{
34+
}
35+
36+
var assemblyPaths = new List<FileInfo>
37+
{
38+
new FileInfo(Path.Combine(packageFrameworkPath, "1.dll")),
39+
new FileInfo(Path.Combine(packageFrameworkPath, "2.dll"))
40+
};
41+
var probingPaths = new List<DirectoryInfo> { new DirectoryInfo(packageRootPath) };
42+
43+
var mockPackageRestoreContextWrapper = new Mock<PackageRestoreContextWrapper>();
44+
mockPackageRestoreContextWrapper
45+
.SetupGet(m => m.ResolvedPackageReferences)
46+
.Returns(new ResolvedPackageReference[]
47+
{
48+
new ResolvedPackageReference(
49+
packageName,
50+
packageVersion,
51+
assemblyPaths,
52+
new DirectoryInfo(packageRootPath),
53+
probingPaths)
54+
});
55+
56+
var packageResolver = new PackageResolver(mockPackageRestoreContextWrapper.Object);
57+
IEnumerable<string> actualFiles = packageResolver.GetFiles(tempDir.Path);
58+
59+
string metadataFilePath =
60+
Path.Combine(tempDir.Path, DependencyProviderUtils.CreateFileName(1));
61+
var expectedFiles = new string[]
62+
{
63+
nugetFile.FullName,
64+
metadataFilePath
65+
};
66+
Assert.True(expectedFiles.SequenceEqual(actualFiles));
67+
Assert.True(File.Exists(metadataFilePath));
68+
69+
DependencyProviderUtils.Metadata actualMetadata =
70+
DependencyProviderUtils.Metadata.Deserialize(metadataFilePath);
71+
var expectedMetadata = new DependencyProviderUtils.Metadata
72+
{
73+
AssemblyProbingPaths = new string[]
74+
{
75+
Path.Combine(packageName, packageVersion, "lib", "framework", "1.dll"),
76+
Path.Combine(packageName, packageVersion, "lib", "framework", "2.dll")
77+
},
78+
NativeProbingPaths = new string[]
79+
{
80+
Path.Combine(packageName, packageVersion)
81+
},
82+
NuGets = new DependencyProviderUtils.NuGetMetadata[]
83+
{
84+
new DependencyProviderUtils.NuGetMetadata
85+
{
86+
FileName = $"{packageName}.{packageVersion}.nupkg",
87+
PackageName = packageName,
88+
PackageVersion = packageVersion
89+
}
90+
}
91+
};
92+
Assert.True(expectedMetadata.Equals(actualMetadata));
93+
}
94+
}
95+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.DotNet.Interactive;
11+
using Microsoft.DotNet.Interactive.Commands;
12+
using Microsoft.DotNet.Interactive.CSharp;
13+
using Microsoft.DotNet.Interactive.Utility;
14+
using Microsoft.Spark.Interop;
15+
using Microsoft.Spark.Sql;
16+
using Microsoft.Spark.Utils;
17+
18+
namespace Microsoft.Spark.Extensions.DotNet.Interactive
19+
{
20+
/// <summary>
21+
/// A kernel extension when using .NET for Apache Spark with Microsoft.DotNet.Interactive
22+
/// Adds nuget and assembly dependencies to the default <see cref="SparkSession"/>
23+
/// using <see cref="SparkContext.AddFile(string, bool)"/>.
24+
/// </summary>
25+
public class AssemblyKernelExtension : IKernelExtension
26+
{
27+
private const string TempDirEnvVar = "DOTNET_SPARK_EXTENSION_INTERACTIVE_TMPDIR";
28+
29+
private readonly PackageResolver _packageResolver =
30+
new PackageResolver(new PackageRestoreContextWrapper());
31+
32+
/// <summary>
33+
/// Called by the Microsoft.DotNet.Interactive Assembly Extension Loader.
34+
/// </summary>
35+
/// <param name="kernel">The kernel calling this method.</param>
36+
/// <returns><see cref="Task.CompletedTask"/> when extension is loaded.</returns>
37+
public Task OnLoadAsync(IKernel kernel)
38+
{
39+
if (kernel is CompositeKernel kernelBase)
40+
{
41+
Environment.SetEnvironmentVariable(Constants.RunningREPLEnvVar, "true");
42+
43+
DirectoryInfo tempDir = CreateTempDirectory();
44+
kernelBase.RegisterForDisposal(new DisposableDirectory(tempDir));
45+
46+
kernelBase.AddMiddleware(async (command, context, next) =>
47+
{
48+
if ((context.HandlingKernel is CSharpKernel kernel) &&
49+
(command is SubmitCode) &&
50+
TryGetSparkSession(out SparkSession sparkSession) &&
51+
TryEmitAssembly(kernel, tempDir.FullName, out string assemblyPath))
52+
{
53+
sparkSession.SparkContext.AddFile(assemblyPath);
54+
55+
foreach (string filePath in GetPackageFiles(tempDir.FullName))
56+
{
57+
sparkSession.SparkContext.AddFile(filePath);
58+
}
59+
}
60+
61+
await next(command, context);
62+
});
63+
}
64+
65+
return Task.CompletedTask;
66+
}
67+
68+
private DirectoryInfo CreateTempDirectory()
69+
{
70+
string envTempDir = Environment.GetEnvironmentVariable(TempDirEnvVar);
71+
string tempDirBasePath = string.IsNullOrEmpty(envTempDir) ?
72+
Directory.GetCurrentDirectory() :
73+
envTempDir;
74+
75+
if (!IsPathValid(tempDirBasePath))
76+
{
77+
throw new Exception($"[{GetType().Name}] Spaces in " +
78+
$"'{tempDirBasePath}' is unsupported. Set the {TempDirEnvVar} " +
79+
"environment variable to control the base path. Please see " +
80+
"https://issues.apache.org/jira/browse/SPARK-30126 and " +
81+
"https://github.com/apache/spark/pull/26773 for more details.");
82+
}
83+
84+
return Directory.CreateDirectory(
85+
Path.Combine(tempDirBasePath, Path.GetRandomFileName()));
86+
}
87+
88+
private bool TryEmitAssembly(CSharpKernel kernel, string dstPath, out string assemblyPath)
89+
{
90+
Compilation compilation = kernel.ScriptState.Script.GetCompilation();
91+
string assemblyName =
92+
AssemblyLoader.NormalizeAssemblyName(compilation.AssemblyName);
93+
assemblyPath = Path.Combine(dstPath, $"{assemblyName}.dll");
94+
if (!File.Exists(assemblyPath))
95+
{
96+
FileSystemExtensions.Emit(compilation, assemblyPath);
97+
return true;
98+
}
99+
100+
throw new Exception(
101+
$"TryEmitAssembly() unexpected duplicate assembly: ${assemblyPath}");
102+
}
103+
104+
private bool TryGetSparkSession(out SparkSession sparkSession)
105+
{
106+
sparkSession = SparkSession.GetDefaultSession();
107+
return sparkSession != null;
108+
}
109+
110+
private IEnumerable<string> GetPackageFiles(string path)
111+
{
112+
foreach (string filePath in _packageResolver.GetFiles(path))
113+
{
114+
if (IsPathValid(filePath))
115+
{
116+
yield return filePath;
117+
}
118+
else
119+
{
120+
// Copy file to a path without spaces.
121+
string fileDestPath = Path.Combine(
122+
path,
123+
Path.GetFileName(filePath).Replace(" ", string.Empty));
124+
File.Copy(filePath, fileDestPath);
125+
yield return fileDestPath;
126+
}
127+
}
128+
}
129+
130+
/// <summary>
131+
/// In some versions of Spark, spaces is unsupported when using
132+
/// <see cref="SparkContext.AddFile(string, bool)"/>.
133+
///
134+
/// For more details please see:
135+
/// - https://issues.apache.org/jira/browse/SPARK-30126
136+
/// - https://github.com/apache/spark/pull/26773
137+
/// </summary>
138+
/// <param name="path">The path to validate.</param>
139+
/// <returns>true if the path is supported by Spark, false otherwise.</returns>
140+
private bool IsPathValid(string path)
141+
{
142+
if (!path.Contains(" "))
143+
{
144+
return true;
145+
}
146+
147+
Version version = SparkEnvironment.SparkVersion;
148+
return (version.Major, version.Minor, version.Build) switch
149+
{
150+
(2, _, _) => false,
151+
(3, 0, _) => true,
152+
_ => throw new NotSupportedException($"Spark {version} not supported.")
153+
};
154+
}
155+
}
156+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Library</OutputType>
5+
<TargetFramework>netcoreapp3.1</TargetFramework>
6+
<RootNamespace>Microsoft.Spark.Extensions.DotNet.Interactive</RootNamespace>
7+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
8+
<IsPackable>true</IsPackable>
9+
<!-- NU5100 warns that a dll was found outside the 'lib' folder while packaging. DotNet.Interactive expects extension dlls in the 'interactive-extensions/dotnet'. -->
10+
<NoWarn>NU5100;$(NoWarn)</NoWarn>
11+
12+
<Description>DotNet Interactive Extension for .NET for Apache Spark</Description>
13+
<PackageReleaseNotes>https://github.com/dotnet/spark/tree/master/docs/release-notes</PackageReleaseNotes>
14+
<PackageTags>spark;dotnet;csharp;interactive;dotnet-interactive</PackageTags>
15+
16+
<RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet3.1/nuget/v3/index.json</RestoreAdditionalProjectSources>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<InternalsVisibleTo Include="Microsoft.Spark.Extensions.DotNet.Interactive.UnitTest" />
21+
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" Key="0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<PackageReference Include="Microsoft.DotNet.Interactive.CSharp" Version="1.0.0-beta.20262.1">
26+
<PrivateAssets>all</PrivateAssets>
27+
</PackageReference>
28+
</ItemGroup>
29+
30+
<ItemGroup>
31+
<ProjectReference Include="..\..\Microsoft.Spark\Microsoft.Spark.csproj" />
32+
</ItemGroup>
33+
34+
<ItemGroup>
35+
<None Include="$(OutputPath)/Microsoft.Spark.Extensions.DotNet.Interactive.dll"
36+
Pack="true"
37+
PackagePath="interactive-extensions/dotnet" />
38+
</ItemGroup>
39+
40+
</Project>

0 commit comments

Comments
 (0)