Creating wrappers for Windows exe files using Homebrew and Wine

Creating wrappers for Windows exe files using Homebrew and Wine

Tags
MacOS
OSS
Software Development
Published
August 22, 2024
Author

Introduction to the Windows software I needed: FORScan

I recently noticed that my 2013 Ford Transit mk7 van doesn’t have great observability to debug what went wrong and it just shows ENGINE MALFUNCTION text in the small screen instead of actually providing helpful error messages. This message is pretty scary, but my car seemed to move forward so I thought it’s mainly to get more money for the Ford service shops.
ENGINE MALFUNCTION warning on the Ford Transit mk7 display and yellow light for stability control system.
ENGINE MALFUNCTION warning on the Ford Transit mk7 display and yellow light for stability control system.
I had learned that it’s pretty easy to diagnose these errors by installing FORScan for my machine and finding compatible OBD2 cable. The only problem was that FORScan only works with Windows machines and I only had Macbook available.

Figuring out how to run FORScan on MacOS

I spend sometime googling how I could get FORScan working on my Macbook and stumbled upon this great discussion in miata.net car enthusiastics. I learned that I would need to:
  1. Install Wine through Homebrew
  1. Find proper device drivers for my USB OBD2 cable
  1. Symlinking the connected USB serial devices to Wine COM ports
I followed their great instructions and was able to get FORScan working but this seemed very tedious way to set it up and I wondered if all of this could be automated somehow with Homebrew so that it would be easier to setup next time for the benefit of myself and others.
Connecting my vLinker FS OBD2 USB diagnostics cable to Ford Transit was easy.
Connecting my vLinker FS OBD2 USB diagnostics cable to Ford Transit was easy.
After installing drivers, connecting the cables, editing Windows regedit and symlinking the devices to Wine I was able to get FORScan to read my car diagnostics. One pipe in the turbo was leaking and after that was replaced the problem went away.
After installing drivers, connecting the cables, editing Windows regedit and symlinking the devices to Wine I was able to get FORScan to read my car diagnostics. One pipe in the turbo was leaking and after that was replaced the problem went away.

Automating FORScan installation steps with Homebrew

I had noticed that Homebrew supports custom taps. Taps are basically 3rd party packages which anyone can create and host in their public github repository called homebrew-tap.
Everything tap related is very well documented by Homebrew: https://docs.brew.sh/Formula-Cookbook.
In order to create a new tap you need to create a git repository called homebrew-tap with folder Casks
# Create new homebrew tap which will host your casks formulaes YOUR_GITHUB_USERNAME="onnimonni" # change this to your username! brew tap-new $YOUR_GITHUB_USERNAME/homebrew-tap # Change directory to the newly created tap. You can see this from the output of the earlier command cd /opt/homebrew/Library/Taps/$YOUR_GITHUB_USERNAME/homebrew-tap
Next I needed to find the URL for the installer. This was easy to do by just right-clicking the installer link and using Copy Link Address feature in Chrome. In my case the installer link was:
notion image
 
With the link to the installer I was able create a new cask for Homebrew using their template:
brew create --tap $YOUR_GITHUB_USERNAME/homebrew-tap \ --set-name FORScan \ --cask "https://FORScan.org/download/FORScanSetup2.3.62.release.exe"
This opens your code editor and automatically fills up the the name and is able to figure out the version and the checksum automatically:
# Documentation: https://docs.brew.sh/Cask-Cookbook # https://docs.brew.sh/Adding-Software-to-Homebrew#cask-stanzas # PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST! cask "FORScanni" do version "2.3.62" sha256 "fad11dd5f91a86961b2435798eeae8241dd3a69c92808f5b9eedfe35eac2a77c" url "https://FORScan.org/download/FORScanSetup#{version}.release.exe" name "FORScanni" desc "" homepage "" # Documentation: https://docs.brew.sh/Brew-Livecheck livecheck do url "" strategy "" end depends_on macos: "" app "" # Documentation: https://docs.brew.sh/Cask-Cookbook#stanza-zap zap trash: "" end
Well this is nice. Next I added proper values for desc and homepage. I also added auto_updates false line into the formula to tell homebrew that this software can’t automatically update itself. If you’re unsure please check guidance from Homebrew docs.

Automatically updating Casks by adding livecheck block

livecheck is needed if you want to be able to easily update the version with Homebrew. I needed to use a bit time to figure this out but FORScan change history page provides links to all versions https://FORScan.org/changes_history.html and I noticed that all download links follow this pattern download/FORScanSetup.
Searching examples with Github code search helped me to figure out how to use regex properly here:
livecheck do url "https://FORScan.org/changes_history.html" regex(%r{href=.*download/FORScanSetup(\d+(?:\.\d+)+)\.release\.exe}) end

Adding other casks as dependencies to your cask

Next I needed to add additional dependencies which we will need in order to get my software running. If your windows software will not need USB serial devices you should not need anything else than wine-stable.
# Since FORScan is made for Windows we need to install Wine to run it depends_on cask: "wine-stable" # Virtual COM Drivers which work for vLinker FS USB OBD2 reader # They also should work for the OBDLink EX which is the other recommended cable # These are needed for the MacOS host machine to detect the OBD2 reader depends_on cask: "ftdi-vcp-driver"

Running the downloaded Windows installer

Remember that Windows software typically doesn’t look like the self-contained Application.app containers we have on MacOS? With Windows you will download a installer which will then create a shortcut to desktop and add the software to your C:/Program Files (x86)/FORScan/FORScan.exe. We have now only told the Homebrew to download this installer.
We can run the installer manually by locating it and running that with Wine:
wine FORScanSetup2.3.62.release.exe
But this doesn’t look very automatic and doesn’t happen in the background. You can ask for Windows installer flags by providing /help as the third parameter:
wine FORScanSetup2.3.62.release.exe /help
Running this command will open a new long window containing all of the different installer options. I thought that I will not want to change the default installation path but I just want it to use defaults and run in the background. By adding following flags I was able to do that:
wine FORScanSetup2.3.62.release.exe /SP- /VERYSILENT
This will run the installer for us and adds the FORScan program files into $HOME/.wine/drive_c/Program Files (x86)/FORScan/FORScan.exe. We can now add a Homebrew installer keyword into our formula. See how I used the staged_path and version variables provided by Homebrew.
# Runs the FORScan Windows installer installer script: { executable: "wine", args: [ "#{staged_path}/FORScanSetup#{version}.release.exe", # Windows EXE installer options here: # Run the FORScan without user input "/SP-", "/VERYSILENT", # Default installation path is C:/Program Files (x86)/FORScan/ ], }

Creating a artificial .app folder to actually launch FORScan

Currently Homebrew can download and run the Windows Installer but we don’t yet have a nice FORScan.app in our Applications folder.
We will need to create a minimal .app structure to actually launch the FORScan. It will not be self-contained and portable to other systems but we don’t care about that. We will need to create a executable into FORScan.app/Contents/MacOS/FORScan and a project metadata file into FORScan.app/Contents/Resources/Info.plist. The end result will look like this:
$ tree FORScan.app/ FORScan.app/ └── Contents ├── MacOS │   └── FORScan └── Resources └── Info.plist
We will abuse next Homebrew keyword preflight to create that but before that add a name for the .app folder which Homebrew will look for into your formula:
app "FORScan.app"
This will also tell Homebrew to move this folder to your Applications. Next let’s create it in the preflight phase of the cask.
# This very tiny bash script will be our installer forscan_launcher_content = <<~EOS #!/usr/bin/env bash # Open the FORScan application itself through Wine open -a 'Wine Stable' "$HOME/.wine/drive_c/Program Files (x86)/FORScan/FORScan.exe" EOS # We need to add a PLIST file to tell MacOS about our application. # I'm not sure if this was actually needed in the end # Source: https://www.artembutusov.com/how-to-wrap-wine-applications-into-macos-x-application-bundle/ forscan_plist_content = <<~EOS <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleExecutable</key> <string>FORScan</string> <key>CFBundleGetInfoString</key> <string>FORScan</string> <key>CFBundleIconFile</key> <string>AppIcon</string> <key>CFBundleIconName</key> <string>AppIcon</string> <key>CFBundleName</key> <string>FORScan</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleSignature</key> <string>4242</string> <key>NSHighResolutionCapable</key> <true/> </dict> </plist> EOS # Create the Forscan.app folder structure and necessary files preflight do FileUtils.mkdir_p "#{staged_path}/FORScan.app/Contents/MacOS" File.write "#{staged_path}/FORScan.app/Contents/MacOS/FORScan", forscan_launcher_content FileUtils.chmod 0755, "#{staged_path}/FORScan.app/Contents/MacOS/FORScan" FileUtils.mkdir_p "#{staged_path}/FORScan.app/Contents/Resources/image.iconset" File.write "#{staged_path}/FORScan.app/Contents/Resources/Info.plist", forscan_plist_content end
Then you are done and should be able to use your Windows software through wine. If you want your users to be able to cleanup the installation you could add uninstall keyword as well into your Cask formula:
uninstall delete: [ "~/.wine/drive_c/Program Files (x86)/FORScan", "~/.wine/drive_c/ProgramData/Microsoft/Windows/Start Menu/Programs/FORScan", ]
After these changes the complete file should look like something like this:
cask "forscan" do version "2.3.62" sha256 "fad11dd5f91a86961b2435798eeae8241dd3a69c92808f5b9eedfe35eac2a77c" url "https://forscan.org/download/FORScanSetup#{version}.release.exe" name "FORScan" desc "Software scanner for Ford, Mazda, Lincoln and Mercury vehicles" homepage "https://forscan.org/home.html" livecheck do url "https://FORScan.org/changes_history.html" regex(%r{href=.*download/FORScanSetup(\d+(?:\.\d+)+)\.release\.exe}) end # Since FORScan is made for Windows we need to install Wine to run it depends_on cask: "wine-stable" # Virtual COM Drivers which work for vLinker FS USB OBD2 reader # They also should work for the OBDLink EX which is the other recommended cable # These are needed for the MacOS host machine to detect the OBD2 reader depends_on cask: "ftdi-vcp-driver" app "FORScan.app" # This very tiny bash script will be our installer forscan_launcher_content = <<~EOS #!/usr/bin/env bash # Open the FORScan application itself through Wine open -a 'Wine Stable' "$HOME/.wine/drive_c/Program Files (x86)/FORScan/FORScan.exe" EOS # We need to add a PLIST file to tell MacOS about our application. # I'm not sure if this was actually needed in the end # Source: https://www.artembutusov.com/how-to-wrap-wine-applications-into-macos-x-application-bundle/ forscan_plist_content = <<~EOS <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleExecutable</key> <string>FORScan</string> <key>CFBundleGetInfoString</key> <string>FORScan</string> <key>CFBundleIconFile</key> <string>AppIcon</string> <key>CFBundleIconName</key> <string>AppIcon</string> <key>CFBundleName</key> <string>FORScan</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleSignature</key> <string>4242</string> <key>NSHighResolutionCapable</key> <true/> </dict> </plist> EOS # Create the Forscan.app folder structure and necessary files preflight do FileUtils.mkdir_p "#{staged_path}/FORScan.app/Contents/MacOS" File.write "#{staged_path}/FORScan.app/Contents/MacOS/FORScan", forscan_launcher_content FileUtils.chmod 0755, "#{staged_path}/FORScan.app/Contents/MacOS/FORScan" FileUtils.mkdir_p "#{staged_path}/FORScan.app/Contents/Resources/image.iconset" File.write "#{staged_path}/FORScan.app/Contents/Resources/Info.plist", forscan_plist_content end uninstall delete: [ "~/.wine/drive_c/Program Files (x86)/FORScan", "~/.wine/drive_c/ProgramData/Microsoft/Windows/Start Menu/Programs/FORScan", ] end
Then just commit your changes and push them to your public Github repository:
cd /opt/homebrew/Library/Taps/$YOUR_GITHUB_USERNAME/homebrew-tap git add . git commit -am "Created FORScan Cask installer to use Windows software on MacOS" git push origin HEAD
After this your Windows software can be installed by running:
brew install --no-quarantine --cask wine-stable $YOUR_GITHUB_USERNAME/tap/forscan
Because the installer of our Cask formula depends on the wine being available in the system we need to give that as the first package to be installed before our custom tap can be used. I hope this was useful for you and will help you to run Windows software on your Mac with ease.

Extras: Automating complicated setup and dependencies

Because FORScan needs to use the USB serial device attached to the host machine these need to be symlinked to virtual Wine COM devices I needed to add a lot of extra ducktape and glue to make that experience easy for the users of my tap. I replaced the bash based launcher with Applescript which takes care that the necessary system extensions will be installed and the USB devices are connected and symlinked properly before actually opening the FORScan. There’s also some Windows regedit changes needed before Wine can see the USB devices.
If you’re curious you can check the complete Cask formulae I created in my own tap: https://github.com/onnimonni/homebrew-tap/blob/main/Casks/forscan.rb
It was a pretty fun experiment of how one can open programs and click their buttons automatically for the user. Now I don’t need to remember how to make this complicated setup manually anymore. It was a pretty complicated and time consuming journey but hopefully this will allow others to achieve same results easily in the future 🤝.