apple / swift-argument-parser

Straightforward, type-safe argument parsing for Swift
Apache License 2.0
3.34k stars 321 forks source link

AsyncParsableCommand exits before running `run() async` #538

Open julianiaccopucci opened 1 year ago

julianiaccopucci commented 1 year ago

When using an AsyncParsableCommand with @main, the command-line-tool exits without running the run() async function and prints the help log. Note: run() without async 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.6

Checklist

Steps to Reproduce

Option A: Reproduce by running the Example in this repository

Option B: Screenshot 2023-01-06 at 10 36 30

Expected behavior

The run() async function should run

Actual behavior

The run() async function isn't run and the program exits with the help log.

julianiaccopucci commented 1 year ago

Just to show what works and what doesn't. Running it from Xcode or command line:

This works: Screenshot 2023-01-06 at 11 24 12

These three don't work: Note that number three is not async, but the @available is specified at the function level. 1)------------------------------ Screenshot 2023-01-06 at 11 28 50 2)------------------------------ Screenshot 2023-01-06 at 11 29 11 3)------------------------------ Screenshot 2023-01-06 at 11 28 19

julianiaccopucci commented 1 year ago

I'm using Swift 5.6, so I need to create a standalone type as your asynchronous @main entry point.

julianiaccopucci commented 1 year ago

Reopening it as the documentation of AnycMainProtocol indicates to use @main directly as I was using it which causes the issue above.

julianiaccopucci commented 1 year ago

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. Screenshot 2023-01-06 at 14 48 57 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.

mac-cain13 commented 9 months ago

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.

blochberger commented 8 months ago

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()