In the last few months twice I’ve wanted to access the source code of our application. The first time I did it I came up with a pretty neat hack, but it wouldn’t really work in many places. The second time however, I asked the internet, and the internet replied.
TLDR: You can use your project’s scheme to expose derived Xcode environment variables to your source code.
The rest of the blog post is a little bit about why I wanted to do that and what I did with it.
Both times I’ve wanted to access the source code of our apps is because I’ve wanted to make better admin tools. It should come as no surprise to people who know me that I care about tooling, but I also care a lot about making it possible for our admins to do their own thing. As such, our admin settings panel in Eigen is extensive.
Root React Components
The first time came when I started to think about what admin options I’d like to see for people using our React Native side. These are the options I came up with:
There are two interesting things about it:
- We support running any master commit of our React Native code inside Eigen, for Admins, via AppHub
- We allow loading arbitrary React components as an admin.
It’s this last bit that’s interesting, right now I’m working on a new root Gene component (read: new view controller) in Emission, our React Native implementation. As this work has not moved upstream into Eigen, I can access it through a commit on AppHub, and then open it using our custom module loader:
In order to show the available root components (Artist/Home/Gene), we use GitHub’s raw URLs to download the source code of our Open Source apps. Hah, a nice hack right? I created a ARAdminNetworkModel
with an API like this:
@interface ARAdminNetworkModel : NSObject
- (void)getEmissionJSON:(NSString *)path completion:(void (^)(NSDictionary *json, NSError *error))completion;
- (void)getEmissionFile:(NSString *)path completion:(void (^)(NSString *fileContents, NSError *error))completion;
@end
Which simply uses NSURLSession
under the hood:
- (void)getEmissionData:(NSString *)path completion:(void (^)(NSData *data, NSError *error))completion;
{
NSURLSession *session = [NSURLSession sharedSession];
NSString *urlFormat = @"https://raw.githubusercontent.com/artsy/emission/master/%@";
NSString *url = [NSString stringWithFormat: urlFormat, path];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
completion(data, error);
}];
[task resume];
}
Nothing special, but it required a cognitive jump to get there.
Submodule Introspection
The second time I wanted this is inside the example app for Emission. This is a typical example application for a library made by pod lib create
. This example app is basically just the admin settings panel from Eigen, shown above.
When I switched the example app to use a similar theme and menu DSL as Eigen, I also took the chance to expand on the buttons we had available. Previously there was the ability to load the view controller for one specific artist, but I knew we had a giant list of artist slugs inside one of our optional sub-modules. What I wanted to do, was offer a random Artist from that if the submodule was init
‘d.
This required introspecting the source, which I could have also done via the GitHub API, but it was also feasible to do by accessing the filesystem outside of the simulator. This is totally possible ( and is how FBSnapshots works ) but I needed to access the project root, then I could build relative links. Thus, I asked the internet. I knew these variables existed, but that they were a part of the build process - and not exposed to the app runtime.
There are two ways to do it, both make sense for different contexts:
- Baking the value into your Info.plist - which makes it available for all consumers at runtime, e.g. you could deploy this value, but it’s not too useful for my problem.
- Exposing it as an environment variable via your scheme - perfect for this case, the variable won’t be exported when you deploy.
Now our scheme looks like this:
</div></div><div class='meta-container'><header> </header></div><div class='date-container'> </div><div class='content-container'><div class='entry-content'>
I can then use the value of SRCROOT
as the start of an absolute path to get to any of the source code in our project. Making the final code:
- (ARCellData *)jumpToRandomArtist
{
NSString *sourceRoot = [NSProcessInfo processInfo].environment[@"SRCROOT"];
NSString *artistListFromExample = @"../externals/metaphysics/schema/artist/maps/artist_title_slugs.js";
NSString *slugsPath = [sourceRoot stringByAppendingPathComponent:artistListFromExample];
NSFileManager *manager = [NSFileManager defaultManager];
// Don't have the submodule? bail, it's no biggie
if (![manager fileExistsAtPath:slugsPath]) { return nil; }
// Otherwise lets support jumping to a random Artist
return [self tappableCellDataWithTitle:@"Artist (random from metaphysics)" selection: ^{
NSString *data = [NSString stringWithContentsOfFile:slugsPath encoding:NSUTF8StringEncoding error:nil];
... and so on
Tooling
Paying attention to your admin tools, and improving your development experience for the whole team is a great way to win friends and influence people. Introspecting your source code may help that.