In-Package Utilities - is there a way to bypass SwiftPM's sandboxing?

I'm creating a library. As part of that library, I need to build a tool which generates some static data.

At first, I started with a Swift script. And that was fine in the beginning. As the tool grew, I decided it would be better structured as multiple files (and could probably do with some tests), so I turned it in to an executable package, which I can run using swift run.

Now I'm finding that it would be really great to share some definition files between the tool package and the parent library package. But it's awkward:

  • At first, I tried a symlink. Xcode can't handle that at all, and keeps complaining that it can't save files because they "don't exist". The compiler and debugger generally seem ok with it, but Xcode somehow keeps 2 versions of the file around and I've had instances where I could swear it was providing an old copy to the compiler. So no symlinks.

  • Next, I'm trying to keep the "real" copy of the file in the library, and include it in the tool via SwiftPM's sources: parameter. So I add a source file like this:

    sources: [ "../../../Sources/ParentModule/SharedTypes.swift" ]
    

    That results in a SwiftPM error: "File must be within the package directory structure."

The SwiftPM approach seems like the "right" thing to do, but the sandboxing seems to be unnecessary, and rules out all sharing between in-package tools and their parent libraries. Is there any way for a compile-time executable utility like mine to do that?

I don't believe this has anything to do with the new plugins features. The tool I'm describing here is to be used by the library maintainers every year or so and the updated tables will come in a new release. It's not the kind of thing clients should care about.

I don't care if I need to add a --unsafe-disable-sandbox flag or whatever. I just don't know if there is such a flag for this case.

Thanks!

Karl

EDIT: I guess these are not strictly "in-package", but more like separate, child packages in the same repository. Is there any way for those things to share files?

SwiftPM only sandboxes the manifests and plugins. The actual products have full freedom to interact with the network and file system, so swift run should not be hampered.

That said, it seems that what you really mean by “sandbox” is the simple check that sources are actually in the package. I guess for a package that will only ever be a local path‐based dependency it might work, but in most cases the package is bundled up by source control or a registry and then fetched to all sorts of clients. Anything located outside the package would be missing, and thus there is a safety net for newbs.

In theory this is a command plugin. You make an executable target either way. Then if you can live inside the minimal requestable permissions available so far, then you can wrap it in a plugin to tie it into SwiftPM. Otherwise you just use swift run. If it is in the local package, clients will not see it as long as you do not create a product for it. A first‐level dependency ought to behave nearly identically for both a plugin and a swift run executable, as long as there are exposed products. Such dependency products will be invisible to transitive clients. (If the first‐level dependency swift run becomes deprecated in favour of plugins, and you cannot live with a plugin sandbox, then turn it into a library and create an executable in the top‐level package that is merely @main extension Implementation {}.)

The traditional way to share between packages would be to move the shared functionality down into a library in another lower package depended on by the primary two.

If that is a problem for some reason, another strategy would be to put the master copy in the lower package as a resource, then use a build tool plugin to copy it as a “generated” source file into to whatever target, and expose that same build tool plugin for the higher level package to use to embed the same “generated” source file. But watch out that Xcode currently has trouble applying build tools to cross‐compiled platforms like iOS.

It sounds like one package is at the repository root (since you speak of clients) and the other is hidden deeper in the repository for developers to use (roughly like the package‐info example in SwiftPM itself)? If so, then the best answer is probably to move the targets from the hidden package into the main one and just create no products for them.

When the presence of an executable causes issues for iOS (grr... once upon a time this actually worked just fine), then I usually convert it into a test, and deliberately misspell it tesWhatever() when I check it in so that it does not run all the time. Then when I do want to run it, I temporarily fix the “typo” and launch the test. See here for a real example. It seems absurd, but it works.

1 Like

Yes, there is a parent library, and within its root folder, alongside its Package.swift, is a subfolder TheTool, containing its own Package.swift.

The difference with package-info is that the tool is not depending on a product from the parent package; I only want to share a single file. I could do that, though. It just seems strange that I can reach up the file hierarchy to grab entire packages, but not individual files. Surely there must be a flag to say I know what I'm doing and accept the risks.

That is also a really interesting idea, thanks! I really wish I didn't have to use it, but it's a clever workaround.

Do I correctly understand that neither package directly references the other as far as the manifests are concerned, but one is entirely within the other as far as the file system is concerned? Then can you not put the shared file(s) inside a dedicated subdirectory of the inner package target (such that the inner package works normally) and then give the outer package a target with an unconventional path pointing at it? SwiftPM does this with the package‐info example to ensure it builds in CI and does not fall out of date.

If you absolutely need the file to not be even a separate target, then you can instead set the path of the target that should contain it to the repository root and then specify a narrower concrete list of sources. There is an obsolete example of this strategy here, although it has since been converted to being copied by a plugin due to the warnings from excluding stuff that is not always present; it worked better before SwiftPM thought everything was a resource.

1 Like