Swift Scripting hashbang

Hi,

I am trying to create a swift script that I can use during the Build Phase in Xcode.

Attempt made so far:

  1. Go to Xcode > Build Phase > Add Run script
  2. shell: #!/usr/bin/swift sh
  3. let x = 10

When I run I get the following error

let: =: syntax error: operand expected (error token is "=")
Command PhaseScriptExecution failed with a nonzero exit code

Questions:

  1. Could you assist me to create a build script (like x=10, just a proof of concept)
  2. Is the hashbang used correct and am I supposed to use it in the shell text field in Xcode ?
  3. Even if I am able to execute a swift function that would really help as I have written some build phase script in swift and would like to execute it.

Note:

  • I even tried on the first line of the script and still throws the same error.

Thanks.

1 Like

You don't need the hashbang and sh, just use /usr/bin/swift to execute Swift code from the script field. There seem to be some issue in that XCode does not recognize that the code is Swift, so it will highlight it as a shell script. It also gives me the following warning: using sysroot for 'iPhoneOS' but targeting 'MacOSX'. No idea how to solve that, but the good news is that it works.

1 Like

That last bit is SR-9216. There are workarounds but they involve hardcoding the path to the macOS SDK.

1 Like

Ah gotcha. I guess the reason it worked for me is that I ran the script form an Aggregate target, instead of the main iOS target. I also tried executing a standalone Swift script from shell in Run Script, but that provided a whole bunch of errors also pointing to the same issue.

However, digging deeper it looks like using xcrun would provide the answer. When setting Shell to /usr/bin/xcrun --sdk macosx swift the script is run without warnings, even when run during an iOS build for a physical device.

3 Likes

Thanks a lot @Matthias and @jrose

I used /usr/bin/xcrun --sdk macosx swift as the shell and now it doesn't throw an error and runs, thanks a ton !!

I am so thrilled that it works:

  • print statements works !! (Report Navigator (left panel) - Expand run script message)
  • FileManager.default.currentDirectoryPath gives the current directory path
1 Like

@Matthias @jrose

Intention:

If something goes wrong I want the build phase to fail and be able to make sense of it.

Example: I am reading from a file and the file may not exist at the file path.

Questions:

  • What is the proper way to fail a script ?
  • Should I throw a fatalError("something") ?

If your script exits with a non-zero exit code and prints to stderr using the format described in the below quote, Xcode will interpret and display it as an error/warning.

This is from a SO answer (perhaps someone knows where the official documentation for this is?):

In shell build phases you can write to stderr using the following format:

<filename>:<linenumber>: error | warn | note : <message>\n

It's the same format gcc uses to show errors. The filename:linenumber part can be omitted. Depending on the mode (error, warn, note), Xcode will show your message with a red or yellow badge.

If you include an absolute file path and a line number (if the error occurred in a file), double clicking the error in the build log lets Xcode open the file and jumps to the line, even if it is not part of the project. Very handy.


That is correct if I remember correctly (I used this functionality successfully in a Swift (build phase run) script that processed Swift source files (a bit like gyb but in Swift) before compilation some years ago).

Thanks @Jens, I am using a swift script file, so what command should I invoke ? (Pardon my ignorance I am new to scripting and scripting in Swift).

Fatal error works and does throw an error (with a red exclamation mark) in Xcode. Just wanted to know if that is how error handling is done for swift scripts or there is some other preferred way.

For example the following run script works for me (resulting in Xcode showing an error which points to the first line and column of my command line app's main.swift):

import Foundation

struct StderrOutputStream: TextOutputStream {
    mutating func write(_ string: String) { fputs(string, stderr) }
}

var standardError = StderrOutputStream() 

print("/Users/me/path_to_my_projects_main.swift:1:1: error: My error message", to: &standardError)

exit(1)

(Note that line and column number is given there, which the quote in my previous reply failed to mention.)

1 Like

@Jens Thanks for pointing it out, I didn't know about TextOutputStream.

When Using Fatal Error:

  • An error like the below is thrown
    Fatal error: yourText: file /Users/SomeUser/SomeProject/SomeTarget/Script-96047C9D22557DA30008A1EA.sh, line 37
  • File name and line numbers are picked automatically
  • It also helps to tap on the left panel and navigate directly to the build phase causing the error
  • On the left panel it throws an error Command PhaseScriptExecution failed with a nonzero exit code
  • Since it is fatal error the control flow is also convenient in guard statements

Sure, there's no reason to not use fatalError in cases where it works.

Using print to stderr with custom file path etc is necessary if your script is meant to signal an error in another file than the script itself (ie a file that the script is processing).

1 Like

Thanks @Jens, I didn't think that far, didn't realise there could be other scripts. Thanks.

There's a bit of official documentation in Xcode's Help (Help > Xcode Help) under "Write a build phase script", but it just mentions "error:" and "warning:" (the minimum needed to get diagnostics to appear in the Issue Navigator) and nothing about the file/line syntax. Can you file a bug at https://bugreport.apple.com to document that properly?

2 Likes

Filed 49608910

2 Likes

Thanks @Jens and @jrose

Was just drafting it, thanks for filing the bug.

This is very interesting just printing it with a prefix of error: and warning: causes it to be caught. Thank you so much guys, learnt quite a lot about scripting.

It would be really cool if swift.org blog could have an article on how swift could be used in scripts.

1 Like

Filed a bug for Build Phase > Run Script not syntax highlighting / formatting (indenting) / code completion for Swift code.

Bug ID: 49609605

1 Like

I have been using Swift scripting in Xcode (Build Phase > Run Script)
I normally type in the swift code in the Run Script box and it works well.

Question:

  1. Is it possible to have a swift script in the project that I can drag and execute instead of typing the entire script in the text box?
  1. Save a .swift file somewhere (with #!/usr/bin/swift at the top).
  2. Make sure it is executable:
    chmod +x MyScript.swift
    
  3. In a build phase, execute it from the standard shell:
    path/to/MyScript.swift
    
1 Like

Thanks a ton @SDGGiesbrecht !!! Saved my day.

I have written down the steps I followed:

  1. Create TestScript.swift file with the shebang #!/usr/bin/xcrun --sdk macosx swift
  2. In TestScript.swift write any swift code that you want to execute
  3. Don't add TestScript.swift to your project
  4. chmod +x TestScript.swift
  5. Tap on Project > Build Phase
  6. Add a new Run Script Phase
  7. In the Shell type /bin/sh
  8. Then in the box below that execute the swift file as shown below:

./SwiftScriptInBuildPhase/TestScript.swift

Note: The path is relative to to the project's root directory, in the above example project contained /SwiftScriptInBuildPhase directory

1 Like

Yeah, the xcrun --sdk macosx part (to prevent compilation for iOS) is presumably because Xcode is providing project environment variables to the script. Those end up misdirecting the compiler that gets launched within the script phase. It should not be necessary when your project is targeting the host platform (macOS) anyway.

1 Like