Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract NativeSourcePlugin #2572

Open
wants to merge 45 commits into
base: main
Choose a base branch
from

Conversation

armanbilge
Copy link
Member

This PR branches off of #2566 to try extracting out a plugin for native sources as suggested in #2566 (comment).


}

case object RustSourcePlugin extends NativeSourcePlugin {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept this here for now, but I guess the idea is this can move into another repo/library? :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would be the main goal. In my option, the perfect solution

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree :) It can be complicated to modify the CI in the primary repo to accomodate rust's special requirements. I think all the rust integration can live very naturally in another repo: a compiler plugin, some std library interop like you suggested, etc.

Comment on lines 11 to 17
trait NativeSourcePlugin {

def extensions: Seq[String]

def compile(config: Config, path: Path): Option[CompilationContext]

}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new interface for compiling native sources.


}

sealed abstract class ClangSourcePlugin extends NativeSourcePlugin {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope this is not over-engineered 😅

Comment on lines 88 to 92
/** Assembler file extension: ".S" */
val assemblerExt = ".S"

/** List of source patterns used: ".c, .cpp, .S" */
val extensions = Seq(cExt, assemblerExt)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure where .S should go, so I snuck it in here.

Also need to fix that comment :)

@@ -56,6 +56,8 @@ sealed trait NativeConfig {
*/
def embedResources: Boolean

def nativeSourcePlugins: Seq[NativeSourcePlugin]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the NativeConfig the right place to keep these?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that could be the only way we could use to pass extensions without the usage of reflection.
I can imagine the following scenario:
We can define an sbt plugin, which could automatically populate some fields of nativeConfig,

object ScalaNativeRustIntegrationPlugin extends AutoPlugin {
  override def requires: Plugins = plugins.ScalaNativePlugin
  
  object autoImport {
   val rustIntegrationConfig = taskKey[RustPLuginConfig](???) 
   val rustIntegrationPlugin = taskKey[RustIntegarionPlugin](???)
   
   case class RustPluginConfig() 
   class RustIntegarionPlugin(config: RustPluginConfig) extends NativeSourcePlugin  
 }
}

and then in users code

val foo = project.in(file("foo")))
  .enablePlugins(ScalaNativeRustIntegrationPlugin)
  .settings(
    rustIntegrationConfig := ???,
    nativeConfig ~= _.withNativeSourceCompilerPlugins(rustIntegrationPlugin.value)
  )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, actually we can make it even easier for the user :)

object ScalaNativeRustIntegrationPlugin extends AutoPlugin {
  override def requires: Plugins = plugins.ScalaNativePlugin
  
  object autoImport {
   val rustIntegrationConfig = settingKey[RustPluginConfig](???) 
   
   case class RustPluginConfig()
 }

  override def projectSettings = Seq(
    nativeConfig := {
      val plugin = new RustSourceCompilerPlugin(rustIntegrationConfig.value)
      nativeConfig.value.withNativeSourceCompilerPlugin(plugin)
    }
  )
}

Then user code is just:

val foo = project.in(file("foo"))
  .enablePlugins(ScalaNativeRustIntegrationPlugin)
  .settings(
    rustIntegrationConfig := ???,
  )

Comment on lines +45 to +46
def rustc: Path = discover("rustc", "RUSTC")
def cargo: Path = discover("cargo", "CARGO_HOME")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the rust integration is extracted, I guess these should be moved/removed as well.

Comment on lines 139 to 140
nativeSourcePlugins =
Seq(CSourcePlugin, CppSourcePlugin, RustSourcePlugin)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put them all here for now, for backwards-compatibility. Maybe in future it should be empty, and default C/CPP plugins are populated elsewhere?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can use a similar strategy as with linktimeProperties - the configurable part would include only custom plugins, while the core ones (C, Cpp,Rust?) would be always part of the core repo and always enabled.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You made another comment:

Maybe, if in the future someone would like to introduce better Cpp integration we could add settings to nativeConfig allowing for disabling our internal implementations.

I think instead of adding more settings, we should keep the C and Cpp plugins as part of the configurable list instead of force-enabling them. They can be included by default but then they will be easy to remove/replace. They should definitely remain part of the core repo always :)

I think the Rust makes sense as another repo.

): Seq[CompilationResult] = {
import NativeSourcePlugin._

val plugins = LlSourcePlugin +: config.compilerConfig.nativeSourcePlugins
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC the .ll files are part of the core functionality—Scala Native will not work without them. So it seems that it should not be optional.

Copy link
Contributor

@WojciechMazur WojciechMazur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job on this topic, I really like the separation of compilation logic depending on the source.
I don't have today more time to spend on it, but it looks promising.

I wonder if in the final implementation we can merge some ideas from #2325 eg. filtering sources.

Comment on lines 111 to 115

def withNativeSourcePlugins(plugins: Seq[NativeSourcePlugin]): NativeConfig

def withNativeSourcePlugin(plugin: NativeSourcePlugin): NativeConfig =
withNativeSourcePlugins(nativeSourcePlugins :+ plugin)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can merge these two by using variadic arguments

Suggested change
def withNativeSourcePlugins(plugins: Seq[NativeSourcePlugin]): NativeConfig
def withNativeSourcePlugin(plugin: NativeSourcePlugin): NativeConfig =
withNativeSourcePlugins(nativeSourcePlugins :+ plugin)
def withNativeSourcePlugins(plugins: NativeSourcePlugin*): NativeConfig =
withNativeSourcePlugins(nativeSourcePlugins ++ plugins)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I wrote these with different purpose.

withNativeSourcePlugin appends a single plugin to your list of plugins.

withNativeSourcePlugins replaces the entire list of plugins. Otherwise there is no way to remove and/or change the order.

I guess it needs better names :)

I have some more thoughts on this topic, in the other comments.

Comment on lines 139 to 140
nativeSourcePlugins =
Seq(CSourcePlugin, CppSourcePlugin, RustSourcePlugin)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can use a similar strategy as with linktimeProperties - the configurable part would include only custom plugins, while the core ones (C, Cpp,Rust?) would be always part of the core repo and always enabled.

@@ -232,6 +248,7 @@ object NativeConfig {
| - optimize: $optimize
| - linktimeProperties: $listLinktimeProperties
| - embedResources: $embedResources
| - nativeSourcePlugins:${nativeSourcePlugins.mkString(",")}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe plugins should define their names (similarly to compiler phases), this way the output would be more friendly.
We can also wrap it into array and add space delimiter for better readability

Suggested change
| - nativeSourcePlugins:${nativeSourcePlugins.mkString(",")}
| - nativeSourcePlugins:${nativeSourcePlugins.mkString("[", ", ", "]")}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this should be improved 👍


}

case object RustSourcePlugin extends NativeSourcePlugin {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would be the main goal. In my option, the perfect solution


import NativeSourcePlugin._

trait NativeSourcePlugin {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about a bit longer name?

Suggested change
trait NativeSourcePlugin {
trait NativeSourcesCompilerPlugin {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that seems good 👍

): Seq[CompilationResult] = {
import NativeSourcePlugin._

val plugins = LlSourcePlugin +: config.compilerConfig.nativeSourcePlugins
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the previous comment - if we would only define additional plugins in NativeConfig we should here define also CLang and Cpp plugins. Maybe, if in the future someone would like to introduce better Cpp integration we could add settings to nativeConfig allowing for disabling our internal implementations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -56,6 +56,8 @@ sealed trait NativeConfig {
*/
def embedResources: Boolean

def nativeSourcePlugins: Seq[NativeSourcePlugin]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that could be the only way we could use to pass extensions without the usage of reflection.
I can imagine the following scenario:
We can define an sbt plugin, which could automatically populate some fields of nativeConfig,

object ScalaNativeRustIntegrationPlugin extends AutoPlugin {
  override def requires: Plugins = plugins.ScalaNativePlugin
  
  object autoImport {
   val rustIntegrationConfig = taskKey[RustPLuginConfig](???) 
   val rustIntegrationPlugin = taskKey[RustIntegarionPlugin](???)
   
   case class RustPluginConfig() 
   class RustIntegarionPlugin(config: RustPluginConfig) extends NativeSourcePlugin  
 }
}

and then in users code

val foo = project.in(file("foo")))
  .enablePlugins(ScalaNativeRustIntegrationPlugin)
  .settings(
    rustIntegrationConfig := ???,
    nativeConfig ~= _.withNativeSourceCompilerPlugins(rustIntegrationPlugin.value)
  )

val inpath = path.toAbsolutePath()

val compiler = plugins
.find(p => p.extensions.exists(ext => inpath.toString.endsWith(ext)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, we might think about improvement for that, eg. building a Map (extension -> impl)

val inpath = path.toAbsolutePath()

val compiler = plugins
.find(p => p.extensions.exists(ext => inpath.toString.endsWith(ext)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we should check if we don't have clashes between plugins when two or more of them would use the same set of extensions

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a good idea :) I was thinking there can be some priority/precedence based on their ordering, but that has got limited use-case and potential for confusion probably :)

val regexExtensions = srcExtensions.mkString("""(\""", """|\""", ")")
private def jarSrcRegex(plugins: Seq[NativeSourcePlugin]): String = {
val regexExtensions =
srcExtensions(plugins).mkString("""(\""", """|\""", ")")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of plugins passing here, maybe we move all of this method to a single Impl taking and plugins list. This might allow for some optimizations also. In most cases, we would probably create at most 2 instances per run, (1 instance per findNativeLibs and findNativePaths call)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, that will be much better 😆

@armanbilge
Copy link
Member Author

I wonder if in the final implementation we can merge some ideas from #2325 eg. filtering sources.

@ekrich do you know what the connection of this PR is to #2325? It wasn't obvious to me.

@ekrich
Copy link
Member

ekrich commented Feb 23, 2022

I haven't had enough time yet to look at this PR. I don't think they are super related. Some of the parts of the build plugin concept had to do with filtering for windows that is now done in a different way and also conditional linking solved some of the things that were needed. I will put a comment on #2325 as an update.

| - dump: $dump
| - optimize: $optimize
| - linktimeProperties: $listLinktimeProperties
| - embedResources: $embedResources
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the extra space here could reduce the diff,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot I renamed the config in question. Actually we need more spaces 😆

case Mode.ReleaseFull => "-O3"
}

def compile(config: Config, inpath: Path): Option[CompilationContext] = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain why this returns an option because in the C/C++ plugin it seems to be not needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question. I'm actually not 100% sure, this was the existing method signature. The rust compiler does return None in some instances. It seems to indicate that there is nothing to be done. But I'm not sure what differentiates it from returning a CompilationContext with an empty command. @WojciechMazur will probably know :)

Comment on lines 132 to 133
compilationOutput.filter(_.isInstanceOf[ObjectFile]) ++
compilationOutput.filter(_.isInstanceOf[Library])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use partition here to avoid 2 transits?

Copy link
Member

@ekrich ekrich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few small things - cosmetic. Will look more later.

jarPattern.matcher(name).matches()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spurious change?

private def readDir(path: Path): Option[Path] =
private def readDir(
path: Path
): Option[Path] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too?

s"glob:${srcPathPattern(path)}**{",
",",
"}"
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here?

private def destSrcPattern(
workdir: Path,
destPath: Path
): String = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants