Why does a Package expose the target's names for import instead of product/library name

Hi there! --

This is my first time posting on the Swift Forums, but I've been following the discussions for quite some time now, and I find everything you say and share to be an infinite resourceful mine of learning opportunities.

The reason I'm posting today is that there is something I cannot get my head around regarding naming products and targets in Package.swift and the subsequent import statements in the code.

The situation

I have two packages:

  • SwiftPM_Libs - a library package containing 3 independent libraries in Sources - Lib1, Lib2 and Lib3
  • SwiftPM_Exec - an executable package importing SwiftPM_Libs as a dependency

Both packages are available here if you want to try for yourself. Clone them in the same directory.

Now here are the directory structure and the Package.swift of both packages:

SwiftPM_Libs

Tree structure

.
β”œβ”€β”€ Package.swift
β”œβ”€β”€ README.md
β”œβ”€β”€ Sources
β”‚   β”œβ”€β”€ Lib1
β”‚   β”‚   └── SwiftPM_Lib1.swift
β”‚   β”œβ”€β”€ Lib2
β”‚   β”‚   └── SwiftPM_Lib2.swift
β”‚   └── Lib3
β”‚       └── SwiftPM_Lib3.swift
└── Tests

Package.swift

// swift-tools-version:4.2

import PackageDescription

let package = Package(
	name: "SwiftPM_Libs",
	products: [
		.library(name: "LibName1", targets: ["Lib1"]),
		.library(name: "LibName2", targets: ["Lib2"]),
		.library(name: "LibName3", targets: ["Lib3"]),
		.library(name: "LibNameAllLibs", targets: ["Lib1", "Lib2", "Lib3"]),
	],
	targets: [
		.target(name: "Lib1", dependencies: []),
		.target(name: "Lib2", dependencies: []),
		.target(name: "Lib3", dependencies: []),
	]
)

SwiftPM_Exec

Tree structure

.
β”œβ”€β”€ Package.resolved
β”œβ”€β”€ Package.swift
β”œβ”€β”€ README.md
β”œβ”€β”€ Sources
β”‚   └── SwiftPM-Exec
β”‚       └── main.swift
└── Tests

Package.swift

// swift-tools-version:4.2

import PackageDescription

let package = Package(
    name: "SwiftPM_Exec",

    dependencies: [
    	.package(path: "../SwiftPM_Libs"),
    ],

    targets: [
        .target(name: "SwiftPM-Exec", dependencies: ["LibName1"]),
    ]
)

What is happening

Now let's say, I am only interested in the classes/functions provided by LibName1, I add the following .target(name: "SwiftPM-Exec", dependencies: ["LibName1"]), to my targets: [].

Now in my code, I would expect to import LibName1 as it is the one I'm referring to in my target's dependencies.

But this doesn't work. Instead I need to import Lib1 which is the name of the target used by the product .library(name: "LibName1", targets: ["Lib1"]), in the SwiftPM_Libs's Package.swift.

This is okay as long as my directories, targets and products have the same name.

What I thought would be happening

My idea was that I could have complexe directory structure, custom target names for internal use, and then expose the result to an outside user through a product and its name. I would expect the final user to have access to my libraries by simply importing the dependency and not having to import all the targets needed by that dependency.

The product .library(name: "LibNameAllLibs", targets: ["Lib1", "Lib2", "Lib3"]) is a good example of that.

I would expect that using .target(name: "SwiftPM-Exec", dependencies: ["LibNameAllLibs"]) would let me import LibNameAllLibs to access them.

I've also tried defining my targets with path: and sources: but it complains about overlapping sources.

Conclusion

Maybe I'm doing something wrong, maybe what I'm doing is an edge case. And in the case I would thankful if you could point me to the right direction.

On the other hand, I think granularity is good thing for package developers, but that for package users, they should only be interested in the package's products and not in the maybe multiple targets used internally by the package.

Is that something "interesting" that could be on the roadmap?

Thanks a lot for your kind help!

Best,
-- Ladislas

4 Likes

To clarify, with the Swift Package Manager,

  • β€œTargets” correspond to what are more generally called β€œmodules”, which are independently importable groups of functionality.* Defining .target(name: "MyTarget", ...) makes something that can be used like import MyTarget. (It does not need to match the directory names or file structure of your product. Specify path or sources to arrange your project differently than the default.)
  • β€œProducts” correspond to the resulting compiled binaries. Defining .library(name: "MyLibrary", ...) would create an actual libMyLibrary.dylib file that can be linked against.† These are also the units that can be selected for the package manager to resolve and build first as a dependency: .target(..., dependencies: ["MyLibrary"])

In the most common case, a package’s libraries and targets will be matched one‐to‐one and likely share their names, but other arrangements are possible.

It is also important to note that not every target needs to be part of a product. They can be kept inaccessible from outside the package.

The way you have it set up you will end up with 4 separate compiled .dylib files, with the contents of each target duplicated in the last one.


In order to accomplish what you want, you will have to use the undocumented attribute @_exported. Your AllLibs would then be a distinct target, which depends on your other three targets and vends them each with @_exported import MyIndividualTarget. Then clients can just reference the AllLibs product and import AllLibs to get access to everything.

See here for a project that does it this way (look at the Package.swift too). That project also provides more fine grained access to each target individually, but you do not have to; you could have a single product containing only the target which has the exports.


* unless there is a main.swift file, turning it into a top‐level executable, which cannot be imported.
† assuming dynamic linking mode.

9 Likes

Thanks a lot Jeremy for the insightful answer!

It makes a lot of sense now that you've explained that "targets" are like "modules".

The @_exported import sounds very interesting, thank for pointing me to your example repo. Not sure I'm going to use it yet but I'll keep it in mind. :slight_smile: