Duplicate .target statements?

On Linux (Debian), I've cloned a project for using SQLite with Swift.

In trying to learn Swift Package Manager a little better, I created a new folder & library package as per the instructions here. All good so far.

When it came time to compile, I cannot figure out how the original Package.swift works though.

Granted, it does have a #if os(Linux) as part of the Package.swift, but I can't figure out how to collapse it into one. Do I need to?

The first posting is for the original file :

// swift-tools-version:4.0
import PackageDescription
let package = Package(
    name: "SQLite.swift",
    products: [.library(name: "SQLite", targets: ["SQLite"])],
    targets: [
        .target(name: "SQLite", dependencies: ["SQLiteObjc"]),
        .target(name: "SQLiteObjc"),
        .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests")
    ],
    swiftLanguageVersions: [4]
)
#if os(Linux)
    package.dependencies = [.package(url: "https://github.com/stephencelis/CSQLite.git", from: "0.0.3")]
    package.targets = [
        .target(name: "SQLite", exclude: ["Extensions/FTS4.swift", "Extensions/FTS5.swift"]),
        .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests", exclude: [
            "FTS4Tests.swift",
            "FTS5Tests.swift"
        ])
    ]
#endif

This was then updated to work with my new package folder ... this also compiles ... as it adheres to keeping the second "package.targets" statement.

// swift-tools-version:5.0
import PackageDescription
let package = Package(
    name: "SwiftSQLite",
    products: [ .library( name: "SwiftSQLite", targets: [ "SwiftSQLite" ] ) ],
    dependencies:
    [
        .package(url: "/tmp/shared0/source/CSQLite", from: "0.0.3")
    ],
    targets: 
    [
        .target(name: "SQLiteObjc"),
        .target(name: "SwiftSQLite", dependencies: ["SQLiteObjc"], exclude: ["Extensions/FTS4.swift", "Extensions/FTS5.swift"] ),
        .testTarget(name: "SQLiteTests", dependencies: ["SwiftSQLite"], path: "Tests/SwiftSQLiteTests")
    ]
)
package.targets = 
[
    .target(name: "SwiftSQLite", exclude: ["Extensions/FTS4.swift", "Extensions/FTS5.swift"] )
]

But in trying to eliminate the second "package.targets" into the main "let package = Package()" statement, it fails and complains about the bridging header in SQLiteObjc.

If you look at the second package.targets statement, it is essentially a duplicate of the first without a dependency on the Objc stuff.

For those of you who visual things differently like I do, here is yet another working version of the Package.swift file ..

// swift-tools-version:5.0
import PackageDescription
let package = Package( name: "SwiftSQLite")
package.targets      = [ .target(name: "SQLiteObjc"), .target(name: "SwiftSQLite", dependencies: ["SQLiteObjc"]) ]
package.targets      = [ .target(name: "SwiftSQLite", exclude: ["Extensions/FTS4.swift", "Extensions/FTS5.swift"]) ]
package.products     = [ .library( name: "SwiftSQLite", targets: [ "SwiftSQLite" ]) ]
package.dependencies = [ .package(url: "/tmp/shared0/source/CSQLite", from: "0.0.3") ]

My question boils down to:
a) why does it take two goes at [ .target(name: "SwiftSQLite"] to make compilation work?
b) how does package.targets = [], immediately followed by package.targets = [] work?

Any insight as to why two "package.targets" statements can work together versus just one would be greatly appreciated, as it implies .target(name: "SwiftSQLite") is in the overall package statement twice?

The package manifest is executed like a normal Swift file.

The original file starts by initializing a Package instance and storing it to the global variable package:

let package = Package(
    /* ... */
    targets: [
        .target(name: "SQLite", dependencies: ["SQLiteObjc"]),
        .target(name: "SQLiteObjc"),
        .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests")
    ],
    /* ... */
)

Then, if the manifest is executed on Linux, it makes some modifications to the Package object...

#if os(Linux)
    /* 1 */ package.dependencies = [.package(url: "https://github.com/stephencelis/CSQLite.git", from: "0.0.3")]
    /* 2 */ package.targets = [
        .target(name: "SQLite", exclude: ["Extensions/FTS4.swift", "Extensions/FTS5.swift"]),
        .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests", exclude: [
            "FTS4Tests.swift",
            "FTS5Tests.swift"
        ])
    ]
#endif

Statement 1 sets the dependencies property of the package instance. Before that line it was unspecified and empty. Now—but only on Linux—it has been modified to include the CSQLite package.

Statement 2 sets the targets property of the package instance to a new value. It uses the property setter directly, completely overwriting the existing array—but again, only on Linux. The differences are the following:

  1. The replacement SQLite target:
    1. does not depend on SQLiteObjc like the original did, and
    2. excludes two files which were previously being used.
  2. The SQLiteObjc target is left out. (This is the reason the original worked, but some of your adjustments didn’t; they failed to remove this entry.)
  3. The replacement SDLiteTests target excludes two files which were previously being used.

To remove all duplication, the same manifest could have been written like this instead:

// swift-tools-version:4.0
import PackageDescription
let package = Package(
    name: "SQLite.swift",
    products: [.library(name: "SQLite", targets: ["SQLite"])],
    swiftLanguageVersions: [4]
)

#if os(Linux)
package.dependencies.append(.package(url: "https://github.com/stephencelis/CSQLite.git", from: "0.0.3"))
#else
package.targets.append(.target(name: "SQLiteObjc"))
#endif

let sqlite: Target = .target(name: "SQLite")
package.targets.append(sqlite)
#if os(Linux)
sqlite.exclude.append(contentsOf: ["Extensions/FTS4.swift", "Extensions/FTS5.swift"])
#else
sqlite.dependencies.append("SQLiteObjc")
#endif

let sqliteTests: Target = .testTarget(name: "SQLiteTests", dependencies: ["SQLite"], path: "Tests/SQLiteTests")
package.targets.append(sqliteTests)
#if os(Linux)
sqliteTests.exclude.append(contentsOf: [
    "FTS4Tests.swift",
    "FTS5Tests.swift"
    ])
#endif

You can also run $ swift package describe and $ swift package dump‐package to see details about the resolved manifest structure. That can be particularly useful when the manifest is heavily conditionalized like this.

1 Like

Oddly, none of the targets actually declare any dependency on a product from the CSQLite package, so it is not being used at all. Loading the manifest on Linux ought to be throwing a warning about that.

:grinning: Many thanks for taking the time to reply! Really appreciate it. :+1:

You took a load off my mind in confirming my suspicion that the second package.target overwrites the first, I kind of thought the Package.swift was still being processed in a normal manner.

With the first one now removed from the file, it still compiles perfectly.

I embedded this SQLite.swift package into a simplistic test and it works nicely, so the CSQLite dependency only comes into play when compiling it into a dependant project.

Cheers for the extra commands on swift package, they've been a welcome addition to see what's going on.