Extensible prebuild plugin (such as SwiftGen)

Hello,

I am trying to actually implement such a plugin for an alternative to SwiftGen.
Applying the plugin, and running it seems to work fine, however I can't manage to get write access to the target sources. Thus said, in the case of SwiftGen or any resource "generated.swift", it is important that the generated file is part of the sources, otherwise there's no autocompletion when using generated content in code.

How are we supposed to do so with SPM plugins ?

Thanks.

  • Q
1 Like

To get write access to the package directory, you would need to convert it to a command plug‐in and set its permissions requirements accordingly.

If you are just want to generate source files for the build will use, that is the purpose of context.pluginWorkDirectory. Anything left there will be treated as though it were in the target’s own directory. (See the SwiftGen example from the proposal, although the API has evolved somewhat since then.)

(Note that you might need to use the command strategy for now anyway if the package supports iOS or related platforms, since Xcode trips over itself when the target platform differs from the host and does not support executable targets.)

1 Like

OK thanks. I started to see the command plugin difference, but then it will not be executed at build time...just manually ?

Then my question is why SwiftGen in the proposal, isn't a command plugin as well... they need to modify sources directly don't they?

Correct. It is designed more for things like formatting code. It just allows us to dodge the iOS bug, with the caveats that we need to remember to manually trigger regeneration, that we need to check the generated files into source control, and that there is nothing stopping the plug‐in from erasing the entire package if we make a mistake writing it.

The example generates separate source files (in pluginWorkDirectory) from those seen by the developer (in Sources). The files from both locations are fed to the compiler together, and thus autocompletion and related code operations do not know the difference. The generated files are temporary and hidden, but within the plug‐in’s sandbox. Since the developer’s permissions and the plug‐in’s permissions are insulated from each other, the developer cannot accidentally edit something that the build will just overwrite again anyway, and the plug‐in cannot overwrite something the developer has not yet checked into source control or otherwise backed up.

Thank you so much for this.

So in the end I should be able to go like the SwiftGen example. I am trying to create such a plugin but for R.swift which generates a R.generated.swift file.

I created an R.swift variant that can handle swift packages and not just xcodeproj. And I am manually trying to make it work locally, using a rswift.artifactbundle that carries the rswift executable along. It is slightly big (23Mb) since it embeds a part of Swift Package Manager itself in order to retrieve a target's resources. The executable works fine on its own on any swift package.

However when I run it through an SPM build tool plugin, I get the following error :

Loading manifest:[debug]: evaluating manifest for 'testpackage' v. unknown 
Loading manifest:[error]: invalidManifestFormat("sandbox-exec: sandbox_apply: Operation not permitted", diagnosticFile: nil)
error: [R.swift] Target 'TestTarget' not found in Swift Package, available targets are: []

So my issue doesn't come from the plugin system but from using my command through the sandbox. I don't understand why though...

Just catching up on this thread, but a couple of thoughts:

As others have said, there are currently two kinds of plugins (build tool plugins and custom command plugins), and only the command plugins have a way to request access to write to the package. But from this and a couple of other questions, it sounds as if it would be a useful extension to also allow build tools plugins to ask for permission to modify the package directory. In the original discussions that seemed like a dangerous thing to do, but as long as permission is given, that does make sense.

That seems like another useful thing to dig into, because after the first build, it should be possible to still get code completion for any generated sources in Xcode. I don't know the details of how that works but I've seen it work in other cases, so it might be possible to generate the sources to the side and still get the code completion. In general it seems cleaner to not have to check in the generated sources.

The intent was to have the generated sources go into a derived-files directory and not the source directory, and IIRC when I tried this I did get code completion after having done a full build once (Xcode was able to see the generated sources).

So it should be possible to do this as a build tool plugin — it could be some detail that's preventing Xcode from seeing the code.

I think that nested sandboxes aren't allowed on macOS, so if the plugin calls SwiftPM then it would have to pass --disable-sandbox to the inner invocation. The outer sandbox would still be inherited by the swift package subprocess.

1 Like

Unfortunately the plugin doesn't call SwiftPM, it calls a compiled version of R.swift that statically links with SwiftPMDataModel-auto .. I figured how to disable sandbox on that one.

I know go further and get this

Loading manifest:[debug]: evaluating manifest for 'testpackage' v. unknown 
Loading manifest:[debug]: evaluating manifest for 'r.swift.plugin' v. unknown 
Loading manifest:[debug]: evaluating manifest for 'r.swift.library' v. 5.4.0 
top scope:[error]: unknown system error while operating on /Users/quentin/workspace/Luni/sandbox/TestPackage/.build/workspace-state.json

which is a great move forward... even thought still not working :D

I already do have code completion coming from generated sources (with Xcode 13.3 and Swift 5.6). I figured the comment had been an assumption made without trying first. But if that was not the case, then there is some problem involving a narrower scenario.

...which in turn attempts to wield the entire toolchain. That will never work from the sandbox.

The context fed to the plug‐in also contains the package directory and some of the information from the manifest. Is it possible to get your information from there and send it directly to R.swift? That is the route I would look into first.

@SDGGiesbrecht , I managed to configure the manifest loader in my use of SwiftPMDataModel-auto and got it to work. I still have an error showing but the R.generated.swift gets created in the generated sources and it is indeed available to Xcode for completion.

I thought of using the context fed to the plug-in however the package graph seems incomplete there and doesn't provide the resources of a given target.

It is true I could go over the target directory and find resources but that would not take in account for the 'excludes'. R.swift initially uses the Xcodeproj to find the resources of a given target, I extended this tool to be able to work without an xcodeproj : when needed for a Swift Package. The downside of linking against SwiftPMDataModel-auto is both evolution and the size of the output executable (23Mb) but I believe it's a good starting point.

I am wondering however, how am I supposed to do in order to get plugin errors to show-up as actual error in the build process ? And the same question for warnings.

You can see Diagnostics being used in some of the examples (but I have heard it still lacks some of the features people are hoping for and that Xcode does not surface them very well yet).

That seems like a bug — while it's true that the package graph that's given to the plugin will always be rooted at the package to which the plugin is being applied, the resources should definitely be getting included for any packages that are there (except filtered out by the exclude clause). What kind of resources are not being included there? I didn't see any test inputs in the link you posted but maybe missed them.

Exactly, this is the intent of the context that's provided. The plugin API is at a very first version and it's expected that SwiftPM will want to extend it in future releases based on what various plugins need. In this case, is the missing piece the resource files you mentioned? That sounds more like a bug, which should be fixable without new API, and it would be great to have more details.

As @SDGGiesbrecht said, using SwiftPMDataModel-auto in the tool called from the plugin may be a short-term workaround but is likely going to cause problems moving forward since it will try to resolve its own package graph and try to do network access etc. Maybe we can find a workaround to what you need from the context (and then fix the SwiftPM bugs so the workaround can be removed in the future) so that the plugin can avoid bringing in SwiftPMDataModel-auto.

Indeed, the Diagnostics API is intended for this. If Xcode isn't showing those properly, it should fix that (and a bug report on Xcode with the details of the error being misreported would be appreciated). As @SDGGiesbrecht the diagnostics don't yet contain additional information (such as fix-its and other things that would be nice), but should be showing up properly with a message and file and line number at least.

1 Like

Thank you all for your active replies, very much appreciated.

Here is my current implementation of the plugin, and it works. And as pointed, it uses a variant of R.swift that needs SwiftPMDataModel-auto, you may see the use of it in R.swift/RswiftCore.swift at feature/generate-swift-package-resources · quentinfasquel/R.swift · GitHub

There is currently no way to access the SPM's 'underlying target' with a plugin, is there ?

Thanks for sharing the link, this looks like great progress! In answer to the question, no, there isn't — the plugin runs in a separate executable and only has the information that SwiftPM sends to it. But it's expected that this will be extended and will evolve over time — what information would your plugin need from the underlying target?

I would need to access target.resources and target.others and for each resource to be able to know if it's a copy type or process type.

Just if you didn't know : The goal of R.swift is to bring references to the bundle resources in plain code using static variables rather than string literals, making a program resource-safe since they're known at compile time. at runtime there cannot be non-existing resources unless the developer still referenced them as string literals.

It pretty much is an alternative to SwiftGen that adopted the style of Android's R..
It's not as flexible as SwiftGen since it cannot use templates (stencil). But R.swift goes a step further in providing a R.validate() method that must be used in a Unit Test : it will check that there's no missing resources used in storyboards, xibs, etc.

1 Like

I've something that's about to be fully functional (not pushed yet), but using build command instead of prebuild command. Using build command works like a script and warnings/errors come to Xcode as expected, but my command won't always run on build unless I clean.

  • How do I get rebuild to properly trigger warnings on Xcode ?
  • Or how do I get my build command run all the time..

Any help would be great.

For surfacing errors to Xcode you can just print to stdout using a specific format.

I got this from swiftlint:

1 Like

I am a bit stuck. I was given a sample code that is architectured like the following

  • A series of local packages that are split in Core, Feature, UI, etc.
  • An iOS target in a xcodeproj, which depends on the previous packages

Each of the local package that requires resources will want to use R.swift as a plugin so that internally they can do : R.string.localizable.my_key() or R.image.my_image() where each package gets a generated file In plugin working directory.

I figured that a prebuild command doesn't work in that situation since it goes as a global prebuild, not a package prebuild, and will not get executed when building iOS target. A build command gets run along with building the package itself. However... with my plugin calling a version of R.swift that embeds package loading itself, I get the following error :

top scope:[error]: Error Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file “repositories” in the folder “.build”." UserInfo={NSFilePath=/Users/.../path-to-local-package-in-app/.build/repositories, NSUnderlyingError=0x11c6aac40 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}
Loading manifest:[debug]: evaluating manifest for 'feature' v. unknown 
Loading manifest:[error]: invalidManifestFormat("/Users/...

How could I possibly move forward..