DEV Community

loading...

Building standalone OpenGL application without Xcode, with bundled separate shader file

shadoweaver profile image Shadoweaver ・3 min read

Background

I have a "Hello Rectangle" project which runs as expected when invoked in Terminal.app. But I'm having trouble creating a standalone macOS bundle out of it.

Let's just say using full Xcode is out of question. I'm very tight on disk space and I'm not doing iOS dev.
Instead I'm using brewed toolchain, including gcc-8, glfw and glew.

The code base looks like this:

src
├── include
│   ├── draw.hpp
│   └── public.hpp
├── resc
│   ├── rect.frag
│   └── rect.vert
├── draw.cpp
├── init-shader.cpp
└── window.cpp
Enter fullscreen mode Exit fullscreen mode

The actual compile command is:

$ g++-8 -lglew -lglfw -framework OpenGL -o <binary> <sources>
Enter fullscreen mode Exit fullscreen mode

Currently, after make, the binary can be invoked in terminal, providing both shader files are present in the same directory. The file reader in init-shader.cpp accepts just filename without preceding path.

Problem

I don't quite want to expose shader files, and if I read this SO answer right, it suggests either you pack shader files in final release (what I'm trying), or you hard code them in other sources (even more undesired). Plus, I want to make sure when I distribute it as a standalone .app bundle, it can run out of box (as long as lowest OpenGL version requirement is met) instead of having to get all brewed dependencies.

I read through this SO question, attempted the operations mentioned, namely:

  1. Build with the -headerpad_max_install_names flag.
  2. Create Info.plist and structre the directory as macOS dictates.
  3. Find all dylibs needed using otool -L on executable, and recursively use otool -L on them to avoid DLL hell (for a want of macOS-specific name), exclude what Apple have guaranteed, and copy them to a.app/Contents/Frameworks. Four of them are found in my case, all in Homebrew's cellar:
    • libglfw.3.dylib
    • libGLEW.2.1.dylib
    • libstdc++.6.dylib
    • libgcc_s.1.dylib
  4. Use install_name_tool -change to modify the reference to dylibs in binary file to those copied.
  5. Wrap the entry point in Info.plist into a shell script so that working directory is right, wherever it's invoked.

Terminal didn't complain all the way through. But it won't run, whether using open -a a.app or directly invoke the actual binary or that tiny script.

And I found this blog post which implies I also need to invoke install_name_tool -id between (2) and (3), and codesign them between (3) and (4).

But it still won't run.

In the former case, the shader linker says shader programs are corrupted, but compilation will succeed, but compilation log is empty; in the latter case the app crashes and dyld complains.

I suspect the former is mostly right, since when I blindly change that folder name from standard Contents to content (or any other name), it will run if I invoke the binary from terminal, and even when I brew unlink glew and glfw, suggesting the binary knows where to look up dylibs and shader files.

What did I miss?

Probing

At this time my release dir looked like this:

Contents
├── Frameworks
│   ├── libGLEW.2.1.dylib
│   ├── libgcc_s.1.dylib
│   ├── libglfw.3.dylib
│   └── libstdc++.6.dylib
├── MacOS
│   ├── hello-rect    <- the actual binary
│   ├── launcher.sh   <- entry wrapper for working dir as the above link suggests
│   ├── rect.frag
│   └── rect.vert
└── Info.plist
Enter fullscreen mode Exit fullscreen mode

Because the unbundled version obviously looks nowhere but cwd, I thought in bundles they would do the same. Later on Google lead me to believe that I need CoreFoundation API to correctly read files in bundle, and it murdered my day. I learned that there should be a Resources folder in bundle, and then added CF codes, which for some reason always give segfault 11. I even brought up one new question on SO.

The real caveat

By pure chance I saw yet another SO question which finally saved my day. Turns out, if GLFW sees Resources in bundle, it changes working dir to that dir!

Solution

So the take away is this: structure the release folder like following:

Contents
├── Frameworks
│   └── <dylibs>
├── MacOS
│   ├── hello-rect
│   └── launcher.sh
├── Resources
│   └── <shader files>
└── Info.plist
Enter fullscreen mode Exit fullscreen mode

Then, walk through the previous list, without install_name_tool -id or codesign, and we're done.

One final question: is make suitable for wrapping all these into a script?

Discussion (0)

Forem Open with the Forem app