PDA

View Full Version : Distributing Program with its own File Structure in MacOS Application Bundle



Mario Donick
09-01-2011, 02:37 PM
So, hello again, it's time for a new thread, this time regarding this wonderful thing "application bundles" on MacOS.

I already know how an app bundle is structured. Usually, the binary is inside the "MacOS" folder and all graphics, sounds etc. are in the "Resources" folder.

The problem is that my game "LambdaRogue" expects all its graphics, sounds etc. in subfolders inside the game folder, i.e.:



LambdaRogue
|
+--graphics folder
+--sound folder
+--music folder
+--docs folder
+--data folder
+--saves folder
|
+--binary file
+--"lambdarogue.cfg" textfile



I first tried to put all these folders and the cfg file into the "MacOS" folder of the application bundle as well:



LambdaRogue.app
|
+--Contents
+--MacOS
+--graphics
+--sound
...etc...
binary


This did not work, though. So I thought its best to put all these things into the "Resources" folder and write a little shell script which would be executed after



#!/bin/sh
cd "/Applications/LambdaRogue.app/Contents/Resources"
./fprl


So I had the following structure:



LambdaRogue.app
|
+--Contents
+--Resources
| +--graphics folder
| +--sound folder
| +--data folder
| +--docs folder
| +--saves folder
| +--binary file
|
+--MacOS
+--startup shell script (changes into resources folder and executes binary file)


I also changed the "executable" entry in the plist file of the app accordingly. The script is correctly executed, the binary "fprl" is started by the script, but it stops with the error message that it does not find its config file.

So obviously changing into the directory will all my resources was not enough, and this again is then something that works different from Windows and Linux.

(The script works if I call it directly from terminal).

So what do I need to do to make this app bundle work? Do I need to change the source code of the game itself in a way that it does not look for its files in its own directory anymore, but instead in ../Resources ? Or is there an easier way?


Edit: Hm. Making the game looking for its resources in the Resources folder does also only work when the binary is started from a terminal. (I put a "../Resources/" before all filenames referred to in the game). However, it does not work when started by double clicking the app icon.

Edit: I now double clicked the binary from within the app bundle. Here, a terminal window opened which showed me (due to some debug messages in my game) that it was started with the working directory set to my home directory. I then included a SetCurrentDir call in my binary that set the working directory correctly.

But again, this had no effect. Double clicking the bundle brought nothing, but double clicking the binary from inside the bundle started the game.

(Remark: I know that I simply could create a zip and put everything inside and tell the users to start the game from within the terminal, i.e. I could simply distribute the game the Linux way. But app bundles are so elegant.)

Stoney
09-01-2011, 03:52 PM
A structure like this:


LambdaRogue.app
|
+--Contents
+--Resources
| +--graphics folder
| +--sound folder
| +--data folder
| +--docs folder
| +--saves folder
| +--binary file
|
+--MacOS
+--Executable file

is the proper Mac OS way. Although I know a lot of applications who don't use this standard.


You might want to consider to create an extra helper function, something like:


function GetGraphicsPath(): String;
begin
{$IFDEF UNIX}
{$IFDEF DARWIN}
Result := '../Resources/graphics/'
{$ELSE}
Result := 'graphics/';
{$ENDIF}
{$ELSE}
Result := 'graphics\';
{$ENDIF}
end;


And load your graphic file with something like:


LoadAsset(GetGraphicsPath() + 'myImage.png');

Mario Donick
09-01-2011, 04:12 PM
Yes, I did this (not with a function, but with a Const containing '../Resources/' put before all filenames). But to quote myself:



Double clicking the bundle brought nothing, but double clicking the binary from inside the bundle started the game.

Mario Donick
09-01-2011, 06:48 PM
I found an acceptable way for deploying my game for Mac Users, described in the following screenshot:

http://lh6.ggpht.com/_1m_MN9SKC70/TSoQPZC2gdI/AAAAAAAABmY/yJLYkT89FiM/s400/lr-mac-deploying.png

(Full size: http://picasaweb.google.com/md1.hro/LambdaRogueDevelopment#5560274546549227986 )

It is not the 100% solution and it forces the user to install the game into the Applications directory, but I think most users would do this anyway.

Now I only need a place where to upload the 200 MB .dmg file.

michalis
09-01-2011, 11:36 PM
It is not the 100% solution and it forces the user to install the game into the Applications directory, but I think most users would do this anyway.


I know users who routinely don't do this, and drag instead applications to other places they like...

You should instead use CFBundle stuff to get the path of your resources in a proper bundle. For some example code, see "examples/trayicon/" in Lazarus examples. You can go to http://svn.freepascal.org/svn/lazarus/trunk/examples/trayicon/ , download frmtest.pas, and look for {$IFDEF Darwin} code inside.

Pasting here the relevant code:


uses
{$ifdef ver2_2_0}
FPCMacOSAll;
{$else}
MacOSAll;
{$endif}

const
BundleResourceFolder = '/Contents/Resources/';

var
pathRef: CFURLRef;
pathCFStr: CFStringRef;
pathStr: shortstring;
pathMedia: string;
...

pathRef := CFBundleCopyBundleURL(CFBundleGetMainBundle());
pathCFStr := CFURLCopyFileSystemPath(pathRef, kCFURLPOSIXPathStyle);
CFStringGetPascalString(pathCFStr, @pathStr, 255, CFStringGetSystemEncoding());
CFRelease(pathRef);
CFRelease(pathCFStr);

pathMedia := pathStr + BundleResourceFolder;

Mario Donick
10-01-2011, 05:27 AM
Interesting -- such stuff exists? I have looked at the example, I think I understood (the .pas file you mention also includes both variants -- one for MacOS and one for Windows, so that I can clearly see the difference. Thanks!!

But: Why is MacOS so complicated here? Why does a simple reference to the Resources folder with "../Resources" not work from inside the "MacOS" folder? Are these security reasons?

Mario Donick
10-01-2011, 06:28 AM
Okay. The path thing is working. My game crashes because of another reason.

All music and sound effects are played using an external commandline mp3 player (mpg123). This has several advantages, so I want to keep this.

mpg123 is invoked by using a TProcess which I named "ExtProgram". Here's the code where it crashes ONLY when I start the game from the app bundle (it always works when started from command line!)



procedure PlayMusic(SoundFile: string);
begin
StopMusic;
if blUseExternPlayer = True then
begin
ExtProgram.CommandLine := strExternPlayer + ' "' + CONST_DATADIR + 'music/' + Soundfile + '"';

ExtProgram.Execute;
end;
end;


I already have printed the complete ExtProgram.CommandLine in a debug file, to ensure that the path is correct -- it is. But still, the program crashes at the ExtProgram.Execute part.

Why, and why only when started as App bundle? Aren't apps allowed to run other programs?


Edit: A bit closer. Usually, I only passed the name of the extern binary (mpg123) to TProcess. Then the game crashed. When I passed the full path to the binary (i.e. /usr/local/bin/mpg123), the game did not crash -- but obviously mpg123 wasn't running either (because I didn't hear anything), although now both the paths to the binary and to the music file to play were full and correct.

(By the way, I noticed that many examples in the www use TUTF8Process instead TProcess, and they also use a function "FindDefaultExecutablePath" to get the full path of a unix application on a system, but neither TUTF8Process nor FileUtil (where FindDefaultExecutablePath would be included) seem to be available on Mac).

michalis
11-01-2011, 06:23 PM
But: Why is MacOS so complicated here? Why does a simple reference to the Resources folder with "../Resources" not work from inside the "MacOS" folder? Are these security reasons?
"../Resources" is a path relative to the current directory of your running process. I don't know what is the current directory when the program is started by clicking on a bundle.app, I don't know if anything is guaranteed here. (Try printing GetCurrentDir to your log to experiment). Most likely it's not something you expect, and this causes your troubles...


Why, and why only when started as App bundle? Aren't apps allowed to run other programs?
I can't help with this one. It's certainly possible to run another program. A program started by clicking on a bundle runs as a "normal" program, without any additional security restrictions, *as far as I know*.

You can always run a bundle from a terminal, like "open xxx.app". This should work exactly like clicking from Finder. Maybe something helpful will be printed to stderr then.




(By the way, I noticed that many examples in the www use TUTF8Process instead TProcess, and they also use a function "FindDefaultExecutablePath" to get the full path of a unix application on a system, but neither TUTF8Process nor FileUtil (where FindDefaultExecutablePath would be included) seem to be available on Mac).TUTF8Process is inside UTF8Process unit. Both this and FileUtil should be available just fine on Mac OS X. Remember that they are part of Lazarus library (LCL), not built-in FPC units. So add to your Lazarus project file dependency on lcl package (or add relevant paths to your ~/.fpc.cfg if you use FPC from the command-line).

In any case, these are most probably not related to your problems, so I wouldn't worry about them now. TUTF8Process is just a version of TProcess that treats all the strings as UTF-8.

Ingemar
02-04-2011, 09:10 PM
There is no reason to get confused by the application bundle. Just make sure that you set the current directory and you will be fine. You do that like this:

procedure Home;
var
mainBundle: CFBundleRef;
resourcesURL: CFURLRef;
path: AnsiString;
success: Boolean;
begin
mainBundle := CFBundleGetMainBundle();
resourcesURL := CFBundleCopyResourcesDirectoryURL(mainBundle);
SetLength(path, PATH_MAX);
success := CFURLGetFileSystemRepresentation(resourcesURL, TRUE, PChar(path), PATH_MAX);
CFRelease(resourcesURL);
if success then
chdir(path);
end;

This makes the Resources folder your current directory.

Ingemar
03-04-2011, 05:47 AM
All music and sound effects are played using an external commandline mp3 player (mpg123). This has several advantages, so I want to keep this.

mpg123 is invoked by using a TProcess which I named "ExtProgram". Here's the code where it crashes ONLY when I start the game from the app bundle (it always works when started from command line!)



procedure PlayMusic(SoundFile: string);
begin
StopMusic;
if blUseExternPlayer = True then
begin
ExtProgram.CommandLine := strExternPlayer + ' "' + CONST_DATADIR + 'music/' + Soundfile + '"';

ExtProgram.Execute;
end;
end;


I already have printed the complete ExtProgram.CommandLine in a debug file, to ensure that the path is correct -- it is. But still, the program crashes at the ExtProgram.Execute part.

Why, and why only when started as App bundle? Aren't apps allowed to run other programs?


Edit: A bit closer. Usually, I only passed the name of the extern binary (mpg123) to TProcess. Then the game crashed. When I passed the full path to the binary (i.e. /usr/local/bin/mpg123), the game did not crash -- but obviously mpg123 wasn't running either (because I didn't hear anything), although now both the paths to the binary and to the music file to play were full and correct.


You are certainly allowed to run other programs. I do it all the time. However, I never use TProcess, at least not if I need to pass data between the two processes. In such cases i use ptyfork. In other cases I can use fork and exec just like TProcess does.

As you say, with a full path things get more stable. Isn't it the current directory that needs to be known again? And a proper call to chdir would help?