XCode dependency hell

Dear Lazyweb, how do I add dependencies to Xcode without also adding link rules?

Alternately, how do I add platform-specific library linking without also losing dependencies on those libraries?

My project builds a static library, and a bunch of bundles, for all platforms.

The complicated part is, the bundles link against that library on MacOS but not on iOS.

The only way I've found to accomplish that is to remove MyLib.a from the "Link Binary With Libraries" field and instead add it explicitly to the "Other Linker Flags" field like this:

  • Other Linker Flags:
    • Debug
      • Any Mac OS X SDK:     -lMyLib ...
      • Any iOS Simulator SDK:     -undefined dynamic_lookup ...
      • Any iOS SDK:     -undefined dynamic_lookup ...
    • Release
      • ...same...

This is because you can't add an SDK-conditional setting like that on the "Link Binary With Libraries" field in the UI. It's all or nothing.

I still have "MyLib" in the "Target Dependencies" field, and when I build the bundle, it does re-build the library first as needed...

However, if a source file linked into the library was changed but a source file linked into the bundle was not, it does not re-link the bundle against the library on MacOS! It leaves it out-of-date with the wrong code, even though the build log shows a "Link" phase running "clang" to link it. Apparently that's a lie.

I even tried adding a "Shell Script" phase that runs before the "Link" phase and manually checks the dates on the files and deletes the output files that are out of date, but even that didn't make the later link phase re-link. Apparently "the target file doesn't exist" is not enough of a whack upside the head of that lying link phase to tell it that it actually has to link. With this hack, I just end up without a runnable program and have to hit "Build" again, which I guess is better than having the wrong program...

So how do I tell Xcode, "when library A is newer than binary B, binary B needs to be re-linked" -- without also telling it, "link binary B against file A on every platform"?

Tags: , , ,

16 Responses:

  1. Andrew Wilcox says:

    This sounds more like you need multiple targets for each platform. I don't even know how to get a single target to build for MacOS and iOS SDKs, and if you would like to share that magic I could probably use it for my eScape project (OSS cross-plat networking lib)…

    If you don't want multiple targets, have you tried marking the library in Link Binary With Libraries as Optional? That way it can be "missing" on iOS (it's lazy-linked) but still used if present.

    Shell scripts cannot modify the state of the bundle created it seems. I've managed multiple projects that used shell scripts to modify Info.plist files (with git revision info, etc.), and you'd have to Cmd+B to build and then Run separately to have the modified Info.plist file inserted into the bundle. Tl;dr shell scripts are second-class citizens to Xcode and almost worthless.

    Also, you should add dl/dt/dd to your blog's acceptable HTML tags. I was going to make this comment look a hell of a lot nicer using them.

    • jwz says:

      Multiple targets would completely suck because then I'd have 400 of them instead of merely 200.

      I do know how to get a single target to build for both MacOS and iOS:

            Supported Platforms: macosx iphoneos iphonesimulator

      Then if you need different linker flags and stuff, hit the "+" next to "Debug" and specify things differently based on "Any iOS Simulator SDK", etc.

      The library can't be "missing" on iOS, because it is actually used, just not hard-linked into these bundles. (Instead it's hard-linked into the iOS app that dynamically loads these bundles.)

      It may be that the "Shell Script" phase can't effectively modify the Info.plist file in the source directory, but I do have scripts that modify the generated Info.plist under $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX and that works ok.

      • Andrew Wilcox says:

        After a lot of wrestling with Xcode, I finally decided to add "-t" (list all loaded files) to Other Linker Flags.

        Nothing printed.

        The issue here seems to be that Xcode is being "smart" and not even invoking the linker when it sees no source files have changed, even though dependent projects have. This is probably because you haven't set any of the dependent project's targets as "Link With". I would suggest that this is a bug and you should submit a Radar (https://bugreport.apple.com) – but the problem is they would probably close it BYDESIGN, because they are too short-sided to see that people might actually do this (Google is full of people complaining about this, and it seems to be a "new" issue with Xcode 4).

        One solution was on the Apple Developer forums, where they presented a slightly hacky but working (in my test projects) solution of:

        - Add the library as a Link Binary With Libraries
        - Drag the entry from Link Binary With Libraries into the bundle's project
        - Remove the library from Link Binary With Libraries
        - Click the file in the project file list window and turn on the Utilities View

        • jwz says:

          Apparently I'm not cool enough to read that link. What do you mean "into the bundle's project"? "Project" is one of those words where, in the context of Xcode, I never know what it means. And what exactly does this do?

          • Andrew Wilcox says:

            This is what happens when cmd+tab goes haywire. Firefox decided that whatever keystroke I made (I think it was "tab" then "space", which I didn't think space worked as "press button" on Macs) should be the same as "Submit the form before you're finished writing the comment".

            That won't work for you after all, because it was just sneaky-linking the library in. When I removed it fully from linking, the same behaviour (non relinking when static lib changes) started happening again.

      • Andrew Wilcox says:

        After a lot of wrestling with Xcode, I finally decided to add "-t" (list all loaded files) to Other Linker Flags.

        Nothing printed.

        The issue here seems to be that Xcode is being "smart" and not even invoking the linker when it sees no source files have changed, even though dependent projects have. This is probably because you haven't set any of the dependent project's targets as "Link With". I would suggest that this is a bug and you should submit a Radar (https://bugreport.apple.com) – but the problem is they would probably close it BYDESIGN, because they are too short-sided to see that people might actually do this (Google is full of people complaining about this, and it seems to be a "new" issue with Xcode 4).

        Your only option seems to be a clean-and-rebuild every time you change a static library, if you don't want to do a Link Binary With Library addition. The thing is, if you're using these bundles on both MacOS and iOS, why not just link it on both and then re-export the symbols so your iOS apps can use it via the bundle? Might give you a little less dependency hell all around.

        • jwz says:

          What do you mean "re-export the symbols"?

          On MacOS, each bundle needs to have the functions from the library built in to it.

          On iOS, if I leave the library functions out of the bundles, I can link that library in only once (in the bundle-loading app) and each bundle is significantly smaller because I don't have 200+ copies of every function from the library, one in each bundle.

          • Andrew Wilcox says:

            I see, so you're actually using all 200 bundles at once in the same project.

            Yeah, you need to clean-and-rebuild, or use something else. I don't know if the "alternative" IDEs for Mac/iOS development support this workflow, but you might want to check. One popular one that I hate but does have a lot of bells-n-whistles (like Quick Fix Magic that assumes your IDE knows your code better than you do) Xcode doesn't is AppCode. Perhaps you could see if that is intelligent enough to handle this workflow.

            Of course, another of AppCode's "features" is that it can sell new Macs for Apple because it usually has a 3-4x+ larger memory footprint than Xcode. With 400 targets and multiple SDKs you're looking at… probably a new Mac Pro with 48 GB RAM to run it.

          • hattifattener says:

            You shouldn't need to incorporate the support functions into each bundle on MacOS either (ha ha "shouldn't")

            Normally you can either make a dylib or framework to hold all that common code (perhaps with an install-path of @executable_path/blahblah), or you can use the -bundle_loader linker flag to tell the linker that it can resolve undefined symbols in bundles against the app they will eventually be loaded into.

            • jwz says:

              Yeah, but if I did that then the .saver bundles would require an actual installer on OSX, rather than "just double-click it or copy it to "Library/Screen Savers". Avoiding that by putting a copy of the framework inside each .saver bundle would defeat the purpose entirely (overall size would go up, not down.)

              Not really concerned about disk space on OSX. It matters more for iOS.

  2. Charlie Schmidt says:

    Totally ignorant of XCode (and from reading your struggles here, jesus I can only hope to stay that way), but is it possible to have MyLib build as an empty library on the iOS platforms using a huge #ifdef ? Then all 3 could still link against it, but it wouldn't add useless code to the iOS versions?

  3. Lun Esex says:

    This may be relevant to your interests:

    http://textfromxcode.tumblr.com/