Is Swift 5.5 ready for Linux?

As an enthusiast, I'm experimenting with async/await and actor on Linux. I use Xcode to edit the source files over SAMBA. I can test the syntax in Xcode by building, but I swift build and swift run on Linus over ssh in Terminal.

Problem 1

The Xcode build gives...

Concurrency is only available in macOS 10.15.0 or newer

So I add this to Package.swift...

platforms: [.macOS(.v12)], // seems odd for Linux, but it allows concurrency

Problem 2

I have an actor called ActorTACHO which works on macOS, but building on linux gives...

<unknown>:0: error: symbol '$s9ActorTest0A5TACHOC8maxIndexACSi_tYacfcTu' (async function pointer to ActorTest.ActorTACHO.init(maxIndex: Swift.Int) async -> ActorTest.ActorTACHO) is in generated IR file, but not in TBD file
<unknown>:0: error: please submit a bug report (https://swift.org/contributing/#reporting-bugs) and include the project, and add '-Xfrontend -validate-tbd-against-ir=none' to squash the errors

I have no idea what this means, so I'm asking if Swift 5.5 is ready for Linux?

For problem 1 - you're building on macOS with a macOS SDK, which is why you need the macOS specifier.

For problem 2, that looks like a compiler bug. Which version of Swift 5.5 are you using? Swift 5.5.3 is the latest and each point release contains a number of bug fixes

1 Like

Thank you @oxTim

I try to keep both platforms bang up to date.

I'm using the latest macOS 12.1, which is considerably newer than 10.15.0. So why do I need to add platforms: to the Package?

I'm using Swift 5.5.2 on both platforms. If 5.5.3 is released, it hasn't it auto updated Xcode? If it's pre-released, where can I get 5.5.3 for for both platforms.

Sorry if this sounds abrupt, but I am 76 and frustrated with how much harder everything has become since the 1980's.

1 Like

Because other people might be using a different platform when attempting to use the package you write, so even if it works on your computer, it might not on a computer running e.g. macOS 10.14. platform tells the package manager (by extension, the downstream users) when a package is unsupported by the user's system before using/compiling it.

Here: Swift.org - Download Swift

OK, a macOS update has just appeared so I'm now on 12.2. But it isn't helping and there's no sign of a Swift update.

My Package is for personal use to run in Debian BullsEye 64bit Light on Raspberry Pi 4. Swift.org isn't interested in Raspberry Pi - despite the large number of people using them. It's bad enough tracking Swift, so I chose not to get involved with Ubuntu - mainly because of it's unfinished support for GPIO, I2C and 1-Wire.

People running macOS two full versions behind are missing out on all the security updates and should upgrade or loose out on new features.

The thing is, Swift is all about iPhones and Swift-NIO is all about Linux. Two sets of very clever developers not working together. If they were, it would be far less hassle trying write client/server software that doesn't use REST.

I wish I'd started this project in Python.

Apple doesn't always release every Swift point update for macOS. Now that Xcode 13.3 has entered beta with Swift 5.6, it's unlikely they'll ever ship a new build of Xcode 13.2 with Swift 5.5.3 (even though they should). So don't worry about using 5.5.2 on macOS and 5.5.3 on Linux, there were only a few bugfixes in 5.5.3.

To get back to your first problem, you can add the settings it recommends to you package and only activate it when building on Linux.

let swiftSettings: [SwiftSetting]
#if os(Linux)
swiftSettings = [.unsafeFlags(["-Xfrontend", "-validate-tbd-against-ir=none"])]
#else
swiftSettings = []
#endif

Then pass it to your relevant target through the swiftSettings parameter.

1 Like

Thanks for your help, but this is over my head. Where should I put this code and where do I find the SwiftSettings parameter. And is this a temporary fix awaiting Apple to release 5.6?

It goes in your Package.swift, where you define your .target. You can pass the swiftSettings parameter.

I don't know what the cause of this issue is or whether it's fixed in Swift 5.6. You can try the Swift 5.6 development snapshots on Linux to see.

I'll try this tomorrow and let you know.

The Xcode toolchain builds of swiftc are built without asserts so TBD validation is not performed. This looks like it might be a bug on all platforms. Do you have a testcase we can look at? If so, do you mind filing a bug at bugs.swift.org?

In the meantime, the compiler flag suggested in the workaround will disable the verification pass, but I don't know how to pass swiftc compiler flags with swiftpm.

@tomerd/@Ben_Cohen another instance where the assertion behaviour difference between Xcode and OSS toolchains causes confusion and frustration.

5 Likes

For some reason I can't login to report this bug. However, having seen the bug reporting instructions I wouldn't have the patience. I got hooked into Swift because it's promoted as an easy to learn language. My mind is focused towards the application level and I don't have the C experience to start messing around with compiler flags. So here's the code that works just fine on macOS, but won't compile on Linux. If Apple doesn't have a Linux machine, I'll be happy to buy them a Raspberry Pi.

~/ActorTest/Package.swift

// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "ActorTest",
    platforms: [.macOS(.v12)], // seems odd for Linux, but it allows concurrency
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .executableTarget(
            name: "ActorTest",
            dependencies: []),
    ]
)

~/ActorTest/Sources/ActorTest/MainFile.swift

import Foundation

@main
struct MainFile {
    static func main() async throws {
        startActorTACHO()
        while true { sleep(10) } // keep running
    }
}

func startActorTACHO() {
    Task(priority: .background) {
        let maxIndex = 7 // monitor 8 fans
        let actorTACHO = await ActorTACHO(maxIndex: maxIndex)
        var count = 0
        while true {
            try! await Task.sleep(nanoseconds: 333_000_000)
            let rpmList = await actorTACHO.rpmList
            print("\(count) - \(rpmList)")
            count += 1
        }
    }
}

/// Creates a long running task to read the RPM speed of a list of fans
actor ActorTACHO {
    private let maxIndex: Int
    private let formatRpm = "%4i"
    private let defaultRpm = "????"
    
    public init(maxIndex: Int) async {
        self.maxIndex = maxIndex
        rpmList = [String](repeating: defaultRpm, count: maxIndex + 1)
        start()
    }
    
    /// Returns a list of formated speeds
    public var rpmList: [String]
    
    /// Returnn a single formated fan speed
    public func rpmFor(index: Int) -> String {
        return rpmList[index]
    }
    
    /// Loops through all devices and runs forever
    private func start() {
        Task {
            while true {
                for index in 0...maxIndex {
                    rpmList[index] = await rpmFor(index: index)
                    await Task.yield()
                }
            }
        }
    }
    
    /// Counts the number of pulses over a one second period and returns a formated rpm String
    private func rpmFor(index: Int) async -> String {
        var curTime = DispatchTime.now().uptimeNanoseconds
        let endTime = curTime + 1_00_000_000
        var rpm = 0
        while curTime < endTime {
//           gpios[i].onRaising { gpio in
            // this is equivalent to increment by 1 and then
            // dividing the tototal by 2 and multipying by 60
            // i.e. total = (total / 2) * 60
            rpm += 30
//           }
            curTime = DispatchTime.now().uptimeNanoseconds
        }
        rpm = Int.random(in: 4000...5000) // temporary while gpios is commented out
        await Task.yield()
        return String(format: formatRpm, rpm)
    }
}

I need to use concurrency in my SatController/SatServer project which is on my ea7kir GitHub page.

Thank you to those who are responding to this post.

If anyone is following this, I've discovered the bug is with my actor's init.

    public init(maxIndex: Int) async {
        self.maxIndex = maxIndex
        rpmList = [String](repeating: defaultRpm, count: maxIndex + 1)
        start()
    }

Calling start() within init requires init to be an async function. Moving the start() function outside (to be called manually) and removing the async allow the the code to build and run in Linux. This is OK, but it would have been nice to have the actor start the process when instanciated.

Please would someone report this as a bug, because I can't.

It's a separate account from these forums or anything other site. If you create an account and create a best-effort bug, then post the link here, there are folks who go through the bug reporter to triage these reports who will put the right labels on the bug and who can ask for clarifying details if needed. Don't worry about spending extended amounts of time to get the bug report "right" if that's not your thing.

I couldn't find a "best-effort" option, but I think I've reported something. It's called SR-15830.

Thanks.

2 Likes

Thanks for the bug report. I tried your test case. It fails with the same error you observed in Swift 5.5. However I cannot reproduce the issue on Swift 5.6 so I suspect it was fixed.

Thanks for this. I'll wait for 5.6 rather than mess with compiler flags, because I've just discovered that if I run the application like this...

import Foundation

runApplication()

func runApplication() {
    Task {
        let maxIndex = 8
        let actorTACHO = ActorTACHO(maxIndex: maxIndex)
        await actorTACHO.start()
        var count = 0
        while true {
            try! await Task.sleep(nanoseconds: 333_000_000)
            let rpmList = await actorTACHO.rpmList
            print("\(count) - \(rpmList)")
            count += 1
            await Task.yield()
        }
    }
    while true { sleep(10) }
}
.
.
.

.. the problem goes away. Go figure!