Open julianiaccopucci opened 1 year ago
Just to show what works and what doesn't. Running it from Xcode or command line:
This works:
These three don't work: Note that number three is not async, but the @available is specified at the function level. 1)------------------------------ 2)------------------------------ 3)------------------------------
I'm using Swift 5.6, so I need to create a standalone type as your asynchronous @main entry point.
Reopening it as the documentation of AnycMainProtocol indicates to use @main directly as I was using it which causes the issue above.
I don't experience the issue by setting my package minimum target to MacOS 12.6. But it's not clear what the min version is for this package. I think this is still an issue and should be addressed by setting a min version for the project or by fixing it for older versions.
Today I also ran into this issue, my command line tool didn't specify a platform at all in its Package.swift
. This causes the following code to compile perfectly fine, but once it runs it just prints the usage instruction:
@main
struct MyCommand: AsyncParsableCommand
mutating func run() async throws {
print("Hello World" )
}
}
Debugging shows that the main
function of the ParsableCommand
is ran instead of the AsyncParsableCommand
, that doesn't try to call the async variant.
Declaring platforms: [.macOS(.v10.15)]
(or more recent) does make it work.
It would be great if the library can be adjusted so it gives a clear compile error. Or at least the documentation of AsyncParsableCommand
could be updated so this is easier to figure out for people running into the issue.
I ran into this or at least a very similar issue as well. I did not use the main
functions provided by ArgumentParser
but invoked run()
directly.
After looking a bit deeper, I noticed that the default implementation of ParsableCommand.run()
is executed, instead of AsyncParsableCommand.run()
.
Reproducible with the following snippet in a main.swift
file or playground:
import ArgumentParser
struct Foo: AsyncParsableCommand {
mutating func run() async throws {
print("Foo")
}
}
// Like AsyncParsableCommand.main()
do {
var cmd: ParsableCommand = try Foo.parseAsRoot()
if var asyncCmd = cmd as? AsyncParsableCommand {
try await asyncCmd.run()
} else {
try cmd.run()
}
} catch {
Foo.exit(withError: error)
}
The Foo.run()
function is never executed. If you set a breakpoint at the default implementation in ParsableCommand.run()
that will be hit instead.
Note that the compiler warns that the await
expression is useless, since no async
operations occur within.
The underlaying problem seems to be that Foo
has two run()
functions, see simplified:
struct Foo {
func run()
func run() async
}
This overload behaviour was introduced in SE-0296, see specifically the section Overloading and overload resolution, and also discussed in the Swift forum. The overloads are called based on their context. If you are in another async
function, you get the async
overload, if you are in a synchronous context, you get the non-async
overload.
Seems like the main context in main.swift
allows both, which can quickly be confirmed by adding a non-ambiguous async
function:
extension AsyncParsableCommand {
mutating func runAsync() async throws {
try await self.run()
}
}
do {
var cmd: ParsableCommand = try Foo.parseAsRoot()
if var asyncCmd = cmd as? AsyncParsableCommand {
//try await asyncCmd.run()
try await asyncCmd.runAsync()
} else {
try cmd.run()
}
} catch {
Foo.exit(withError: error)
}
Warpping everything into an async
function works as well, which is basically what AsyncParsableCommand.main()
does. For ParsableCommand
there is no conflict and the synchronous function will be run.
There is a similar effect for the main
function. Looking at the @available
annotations in this project's code and the comments in this thread, it seems like macOS 10.15 fixed the @main
wrapper, so that it properly calls the async
main
function. However, if you are using main.swift
and invoke main
directly, the synchronous main
function is preferred, which also triggers failAsyncPlatform()
(with a less helpful error message). So you would need to wrap it into an asynchronous function explicitly:
func main() async {
await Foo.main()
}
await main()
Or disambiguate the overload:
extension AsyncParsableCommand {
static func mainAsync() async throws {
try await main()
}
}
await Foo.mainAsync()
When using an AsyncParsableCommand with @main, the command-line-tool exits without running the
run() async
function and prints the help log. Note:run()
withoutasync
runs the function.ArgumentParser version:
1.2.0
Swift version: swift-driver version: 1.45.2 Apple Swift version 5.6 (swiftlang-5.6.0.323.62 clang-1316.0.20.8) MacOS 12.6Checklist
main
branch of this packageSteps to Reproduce
Option A: Reproduce by running the Example in this repository
Option B:
Expected behavior
The
run() async
function should runActual behavior
The
run() async
function isn't run and the program exits with the help log.