Package Access Level

Not sure this would be an easy thing to do, but what are folks thoughts on introducing a "packageinternal" access level modifier to the existing "public", "private", "internal", and "fileprivate"?

The intention here is that "packageinternal" would be at the same level as "internal" except symbols would be exposed to all targets defined in the same swift package.

The problem I find is that in order to support multiple platforms with SPM, you end up having to shard code across many libraries. It would be nice if there was a way to have "internal" libraries... where you want to expose internal access to companion libraries, but not consuming products.

An alternative maybe is to have SPM have a way of defining "internal library" products? Maybe that's a better approach?

Any target not included in a product is semantically this way already. What is missing is the gatekeeping to actually block its use, since the compiler finds it anyway as an accident of the file system layout.

2 Likes

I think I was unclear on my description of my problem. The issue I have is that I'm trying to create a cross platform library.. let's call it AwesomeLibrary. In a single platform case, I would just declare internal things as "internal" an everything is fine. However, in the cross-platform case, I end up having to do something like this:

  • AwesomeLibrary (contains public interfaces)
  • AwesomeLibrary_iOS (contains iOS specific things)
  • AwesomeLibrary_macOS (contains macOS specific things)
  • (etc for each platform)
  • AwesomeLibrary_Internal (contains common code required by each platform, but not things I want to be public)

The problem is AwesomeLibrary_Internal ends up having to declare all the "internal" things as "public", which makes for awkward transitions and re-thinking of scope when you're moving code from one space to another. (e.g. if you start out making things "internal" in AwesomeLibrary, then decide to move it to AwesomeLibrary_Internal and have to re-scope everything to "public".)

In addition now I have two potential problems:

  • whenever another app/library adds a dependency on AwesomeLibrary, they can import AwesomeLibrary_Internal.
  • a 3rd party app/library has the possibility of importing AwesomeLibrary_Internal when they really shouldn't be relying on it.

Although, as it is, SPM seems to have some issues with platform specifiers within a single Package, so I'm not entirely sure my proposed solution would work either.

1 Like

Assuming your product declaration is...

products: [
  .library(name: "AwesomeLibrary", targets: [
    "AwesomeLibrary",
    "AwesomeLibrary_iOS",
    "AwesomeLibrary_macOS",
    // NOT AwesomeLibrary_Internal
  ])
]

...then...

This is a bug that will hopefully vanish with time. Blocking such imports is what the pull request I linked is about.

This is a legitimate annoyance, in that it is not simply a bug and that it would require new design to resolve. I do not know whether I consider it a problem or not, because you cannot safely move a declaration at any of the other access control levels without putting the same thought into the implications of the new location.

The experimental @_spi feature can be used to create a package‐internal SPI. Have you tried it? If that feature were completed, would it be a satisfactory solution in your mind?

Thanks, yes I've been experimenting with @_implementationOnly. It doesn't appear to do what I need from the tests I've done. It seems like in my previous example, I still must declare AwesomeLibrary_Internal symbols as "public" rather than "internal". But I'll take another look.

What I really want is something like "@internal import" where everything is treated as if it were in the same library and I can access internals.

Oh, I hadn't seen SPI. I'll look into it... (or please share a link if you have it handy)

You will not find much about it, because it is unfinished, but the closest thing to canonical documentation would be here:

That'd be a @testable import?

You can certainly look to the @_spi feature, but I'm not sure why changing from internal to @_spi(...) internal every time you move code from one library to another would be less onerous than changing from internal to public.

Keep in mind that Swift's access control modifier rules are specifically designed to make it easy to switch from internal to public. You are allowed to declare public members inside internal types without warning, specifically so that you can "flip the switch" on a type from internal to public by changing only the access modifier of the type itself.

1 Like

Yes, basically I want the same functionality as @testable but for non-test code. I actually forgot to test whether that works. I just assumed there was a guard that only allowed @testable when running tests.

Yup, I do use the nesting of public, but I still have the issue for global internal symbols.