Build Log Groveling for Fun and Profit: Manual Swift Continued

Jeremy W. Sherman

19 min read

Mar 14, 2017


Build Log Groveling for Fun and Profit: Manual Swift Continued

Manual Swift: Understanding the Swift/Objective-C Build Pipeline walked you through the high-level view of how Objective-C gets access to Swift code:

  • Compile a Swift file to produce an object file and a header file
  • Compile the Obj-C file, which imports the Swift-generated header file, to produce an object file
  • Link all the object files together

Today, we’re going to see what that looks like in practice by examining Xcode build logs.

Why look at Xcode build logs?

The last post was about how you’d build and link code without using Xcode to do the work for you. So why does this article go back to leaning on Xcode?

You’ll probably be doing most of your building using Xcode, so growing acquainted with how it does things will be more applicable to your everyday work than the more academic “If we didn’t have Xcode, what would we do?” question. (Yes, the Swift Package Manager exists as an alternative route to building Swift projects, but, no, we will not be talking about it today.)

Reading how Xcode does its work is a good way to reverse-engineer the build process so you can do it without using Xcode. More usefully, if you learn to see past the noise in the Xcode build logs, you learn to pull out the salient details needed to debug most any build-related issue.

A Minimal Obj-C iOS App

We’re going to start with looking at how a pure Obj-C project comes together.
Here’s the on-disk directory layout of a small iOS app named “ImportSwift”:

├── ImportSwift
│   ├── AppDelegate.h
│   ├── AppDelegate.m
│   ├── Assets.xcassets
│   │   └── AppIcon.appiconset
│   │       └── Contents.json
│   ├── Base.lproj
│   │   ├── LaunchScreen.storyboard
│   │   └── Main.storyboard
│   ├── Info.plist
│   └── main.m
└── ImportSwift.xcodeproj
    ├── project.pbxproj
    ├── project.xcworkspace
    │   ├── contents.xcworkspacedata
    │   └── xcuserdata
    │       └── jeremy.xcuserdatad
    │           └── UserInterfaceState.xcuserstate
    └── xcuserdata
        └── jeremy.xcuserdatad
            └── xcschemes
                ├── ImportSwift.xcscheme
                └── xcschememanagement.plist

11 directories, 12 files

Note that this project has only two Obj-C files to compile:

  • AppDelegate.m
  • main.m

The rest of the files either describe the project and IDE state (everything under the
the app (Info.plist), or get run through their own compilers to spit out assets used by the app (the storyboard files and the .xcassets Asset Catalog bundle).

Building an App

There’s more to building an app than just compiling and linking Obj-C files.
Let’s look at where those steps fit into the overall build flow before
digging in.

Here’s a build log trimmed down to highlight the overall flow:

Build target ImportSwift of project ImportSwift with configuration Debug

Write auxiliary files
write-file ImportSwift.hmap
Create product structure

// We're going to pick up here:
CompileC AppDelegate.o ImportSwift/AppDelegate.m normal x86_64 objective-c
CompileC main.o ImportSwift/main.m normal x86_64 objective-c
Ld normal x86_64
// And stop here.

CompileStoryboard ImportSwift/Base.lproj/Main.storyboard
CompileAssetCatalog ImportSwift/Assets.xcassets
CompileStoryboard ImportSwift/Base.lproj/LaunchScreen.storyboard
ProcessInfoPlistFile ImportSwift/Info.plist

Build succeeded

When it comes to understanding how Obj-C code manages to use Swift code,
we only care about the three steps in the middle of the whole shebang that
compile some C-ish stuff, compile some more C-ish stuff, and finally link it
all together to make an app:

CompileC AppDelegate.o ImportSwift/AppDelegate.m normal x86_64 objective-c
CompileC main.o ImportSwift/main.m normal x86_64 objective-c
Ld normal x86_64

In truth, the two CompileC steps are virtually identical, so we only have
to understand two things:

  • CompileC: How Xcode compiles C-like code
  • Ld: How Xcode links object files into an executable

Let’s get started.

CompileC: Compiling a C File into an Object File

Let’s look at how we compile AppDelegate.m into AppDelegate.o.

Build Step Summary

But first, let’s take a good look at that high-level summary of the build step:

CompileC AppDelegate.o ImportSwift/AppDelegate.m normal x86_64 objective-c

This is hard to read, so let’s work it over to where we can better understand it. To start, let’s convert spaces to linebreaks:


Next, undo the path elisions, which were performed earlier to call attention to the overall flow, so that we can experience the build step summary in its full glory:


Let’s break this down:

  • CompileC: This step is about compiling some C (well, C-ish) stuff.
  • /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ This is the product of this build step, a .o object file.

    We can make this mouthful of a path more meaningful by rewriting it using some of Xcode’s handy-dandy build configuration environment variables as:

    Translated from variable names into words, this says to stick the object file under the object file directory, in a folder specific to the build variant (you’ll probably only ever see “normal” here), in a subfolder specific to the target architecture. Neat and tidy!

  • ImportSwift/AppDelegate.m: Xcode is using the usual “name the parent folder of all the source file for an app named YourAppNameHere YourAppNameHere/” trick, so this is the source file in situ in our project directory.
  • normal: This is that $BUILD_VARIANT setting that came up as embedded in the build product output path.
  • x86_64: And here’s the $CURRENT_ARCH current architecture setting that came up in the same path, yet again. This one says to build for the i3/4/5/6/whatever-86 – x86 – architecture, only the 64-bit version.

    x86_64 stands in contrast to i386, which is the 32-bit version, which isn’t flagged as 32-bit explicitly because why would you ever need more than 32 bits? (And wasn’t that 32- to 64-bit default integer size change a fun migration!)

    If you play around with Linux, you might see the same architecture called amd64 rather than x86_64, because AMD beat Intel to market with a 64-bit variant of the i386 architecture.

    Here, seeing x86_64 also tells you that we’re compiling this code for a simulator rather than the actual device, because all the iOS devices use one flavor of ARM architecture or another.

  • objective-c: This is the language we’re compiling using the C (slash Obj-C) compiler.
  • This happens to match the $DEFAULT_COMPILER build setting (and also, thanks to historical accident, path dependency, and the desire for backwards compatibility, the value for $GCC_VERSION, as well, even though Clang is very much not GCC!). It’s Ye Olde Reverse DNS Identifier, pointing at a version of clang. My current clang version actually self-identifies as part of clang --version as clang-800.0.42.1, so I’m not quite sure what they’re getting at with the 1_0, but the rest makes sense: “This is one of Apple’s compilers, part of the LLVM project, called clang, version 1.0, and it’s, uh, a compiler, as you might have guessed from that earlier ‘compilers’ bit, but let’s just make sure we’re on the same page, OK?”

Command Breakdown

Phew! Now we can actually start to dig into the tool invocations that implement this build step. Note that a lot of this is actually driven by settings in the Build Settings configuration; this project had no customization done in there, so we’ll be looking at the Xcode template defaults, which is most likely what any project you look at will be based on, as well.

Establish working directory
First, the build step establishes the working directory used for the rest of the invocations:

cd /Users/jeremy/Workpad/BNR/ImportSwift

This happens to be the $SRCROOT directory of the project, also known as the root directory holding all the target’s files.

onfigure locale
Then, it sets the language and encoding, in case some locale-aware helper gets clever:

export LANG=en_US.US-ASCII

Xcode is expecting to parse out warnings and errors to annotate your code by parsing literally “warning: filename:linenumber: some text” or “error: filename:linenumber: some text”. If some process invoked during compilation started dumping out “advertencia:” or whatever, Xcode would be a lot less helpful – though it would still be aware of whether the build succeeded or failed, since that depends on the human language independent process exit code convention, where exiting with 0 is A-OK, and anything else is bad news.

Ensure PATH includes platform-specific binaries
Then, it sets the PATH used to resolve a command name, like clang, to an actual path, like /usr/bin/clang:

export PATH="/Applications/"

Each directory in the colon-separated PATH list is searched in order from left to right for a match for the command name, and the first one wins. If no match is found, you get an “Unknown command” error in your shell. (Other stuff can go wrong, too, but “no such number, no such name” is the most common problem you’ll encounter.)

This allows Xcode to control what executable runs. Here’s the list reformatted to be a smidge more readable:

  1. /Applications/
  2. /Applications/
  3. /usr/local/bin
  4. /usr/bin
  5. /bin
  6. /usr/sbin
  7. /sbin

You can see it giving preference to the binaries inside its Platforms folder and its Developer folder, before following with the standard system binary locations. Notably, it does not include any of the paths you might have added on to your own PATH – no Fink /sw/ stuff, no MacPorts /opt/ stuff, and if you set up Homebrew somewhere other than /usr/local, you’re out of luck there, too.

Invoke the compiler
Next, we come to the moment we’ve been waiting for – the actual compiler invocation. And ain’t this a humdinger:

/Applications/ -x objective-c -arch x86_64 -fmessage-length=0 -fdiagnostics-show-note-include-stack -fmacro-backtrace-limit=0 -std=gnu99 -fobjc-arc -fmodules -gmodules -fmodules-cache-path=/Users/jeremy/Library/Developer/Xcode/DerivedData/ModuleCache -fmodules-prune-interval=86400 -fmodules-prune-after=345600 -fbuild-session-file=/Users/jeremy/Library/Developer/Xcode/DerivedData/ModuleCache/Session.modulevalidation -fmodules-validate-once-per-build-session -Wnon-modular-include-in-framework-module -Werror=non-modular-include-in-framework-module -Wno-trigraphs -fpascal-strings -O0 -fno-common -Wno-missing-field-initializers -Wno-missing-prototypes -Werror=return-type -Wdocumentation -Wunreachable-code -Wno-implicit-atomic-properties -Werror=deprecated-objc-isa-usage -Werror=objc-root-class -Wno-arc-repeated-use-of-weak -Wduplicate-method-match -Wno-missing-braces -Wparentheses -Wswitch -Wunused-function -Wno-unused-label -Wno-unused-parameter -Wunused-variable -Wunused-value -Wempty-body -Wconditional-uninitialized -Wno-unknown-pragmas -Wno-shadow -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion -Wbool-conversion -Wenum-conversion -Wshorten-64-to-32 -Wpointer-sign -Wno-newline-eof -Wno-selector -Wno-strict-selector-match -Wundeclared-selector -Wno-deprecated-implementations -DDEBUG=1 -DOBJC_OLD_DISPATCH_PROTOTYPES=0 -isysroot /Applications/ -fasm-blocks -fstrict-aliasing -Wprotocol -Wdeprecated-declarations -mios-simulator-version-min=10.1 -g -Wno-sign-conversion -Winfinite-recursion -fobjc-abi-version=2 -fobjc-legacy-dispatch -iquote /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ -I/Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ -I/Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ -iquote /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ -I/Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Products/Debug-iphonesimulator/include -I/Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ -I/Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ -F/Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Products/Debug-iphonesimulator -MMD -MT dependencies -MF /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ --serialize-diagnostics /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ -c /Users/jeremy/Workpad/BNR/ImportSwift/ImportSwift/AppDelegate.m -o /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/

Yeah, that’s all one line. It’s how Xcode shows it loves you.

Use a specific clang
Let’s take this bit by bit, starting with the highlight reel:


Remember all that work setting up the PATH? Yeah, Xcode isn’t going to trust that. It uses an absolute path to clang, the Obj/C compiler.

Specify language and platform

  -x objective-c
  -arch x86_64

It wants to make sure Clang is 100% on the same page about what language it’s compiling, so it sets it explicitly with -x objective-c.

It also needs to tell clang what architecture to target, so there’s that,


There are various flavors of C that we could glom the Obj- part onto. This says to use the version that corresponds to C99 – the revision of the C standard promulgated in 1999 – with GNU extensions. (Note that clang hasn’t implemened all of GCC’s extensions, and that some GCC extensions are intentionally not implemented. But your favorite GNU extension probably made the cut!)

Specify Obj-C flavor


-fobjc-arc puts the compiler into the modern world by flipping on ARC.
You probably have run into this more frequently (if at all) as the version that turns ARC off, -fno-objc-arc, which some extant library code requires to compile successfully.

The ABI version specified is the modern, non-fragile Obj-C application binary interface. This has to do with what runtime functions the compiler can expect to be available and lets the compiler make a variety of other assumptions about how to generate Obj-C binary code that can interoperate with the runtime and other Obj-C binary code.

-fobjc-legacy-dispatch tells clang what sort of Objective-C message-send
dispatch approach to take.
comments in clang::ObjCRuntime::isLegacyDispatchDefaultForArch(),
it appears that GNUstep uses a newer version, while macOS 10.6+ uses the legacy
dispatch approach everywhere,
though comments for UseObjCMixedDispatch()
would seem to say otherwise.
comments in CGObjCNonFragileABIMac::isVTableDispatchedSelector(),
it looks like there’s been some back-and-forth around using a vtable to
dispatch selectors or not, and the current options are “legacy”, “non-legacy”,
and “mixed”.

Confusion around what flavor of dispatch would be the default for macOS aside,
this is an iOS project, and the dispatch method is explicitly set to “legacy”,
so let’s move on!

rewrite to 0, which is commonly used to represent a Boolean false.
This setting causes the header <objc/message.h> to expose the
core message-sending functions as () -> Void functions,
which forces the caller to cast them to an appropriate type
– as is required for ARC to work its magic –
rather than as varargs (Any, Selector, Any...) -> Any,
which invites trouble under ARC.

Configure some features


-fpascal-strings enables support for Pascal strings.
Pascal used length-prefixed strings rather than NUL-terminated strings.
The Pascal-string support lets you write a Pascal-compatible string like
"pRockin' it Pascal style" and have the compiler replace the p with
the strlen of the string. It also turns the string literal into an explicit
character array literal, as you’ll find as you hammer on the variable

If you don’t pass -fpascal-strings, the compiler rejects the p:

pascal.c:6:37: warning: unknown escape sequence 'p' [-Wunknown-escape-sequence]
static const char PascalString[] = "pWirth";
1 warning generated.

If you declare as const char *, you get a wrist slap for signedness, because
Pascal strings apparently are explicitly unsigned characters:

pascal.c:6:20: warning: initializing 'const char *' with an expression of type
      'unsigned char [7]' converts between pointers to integer types with
      different sign [-Wpointer-sign]
static const char *PascalString = "pWirth";
                   ^              ~~~~~~~~~
1 warning generated.

If you declare as const unsigned char[], you run into signedness problems
using bog-standard const char* APIs:

pascal.c:14:9: warning: passing 'const unsigned char [7]' to parameter of type
      'const char *' converts between pointers to integer types with different
      sign [-Wpointer-sign]
/usr/include/vis.h:105:32: note: passing argument to parameter here
int     strvis(char *, const char *, int);
1 warning generated.

So you’ll find that const char PascalString[] is your friend.

A lot of classic Mac Toolbox APIs used
Pascal calling conventions and expected Pascal strings, but
enabling this support for iOS is something of an anachronism.

Activate modules


This cluster of options relates to Clang’s support for modules.

-fmodules enables modules, module-based imports, and modules-related syntax,
like @import.

-gmodules enables module debugging.

-fmodules-cache-path tells the compiler where to cache module lookup results.
Xcode is building the cache in a folder named ModuleCache in a per-user
derived-data directory.

-fmodules-prune-interval sets a minimum bound on how long the compiler should go before trying to prune the module cache.
Xcode is setting it to 24 hours, which is a lot more aggressive than the compiler default of 7 days.

-fmodules-prune-after says that, if a module cache file hasn’t been accessed in the number of seconds specified, then it can be pruned.
Xcode is setting this to 4 days, rather than the compiler default of 31 days.

-fmodules-validate-once-per-build-session avoids repeatedly validating a
module during a single build session.

-fbuild-session-file names a file whose modification time is treated as the time when the build session started, which matters for some decision-making related to what-to-validate-when for modules.

-Wnon-modular-include-in-framework-module flips on everyone’s favorite warning
when using older frameworks, “warning: include of non-modular header inside
framework module…”. The -Werror flag following turns this into a hard
error, rather than an ignorable one.

Notice that the module cache is not project-specific, but is instead in a
shared location. If you take a peek in your module cache directory,
you’ll see something like:

  • A Session.modulevalidation file, whose contents are self-descriptive:
    “Module build session file for module cache at /Users/jeremy/Library/Developer/Xcode/DerivedData/ModuleCache”
  • A modules.timestamp file, which is empty, but seems to line up with the
    Session.modulevalidation file in terms of its modification time.
  • A bunch of directories whose names are an all-caps mishmash of letters and
    numbers. All the directory names are 13 uppercase alphanumerics characters.
  • Inside each directory is a bunch of files named like $(MODULE_NAME)-$(THIRTEEN_ALPHANUMERICS).pcm. Some of the pcm files might have a suffix of a hyphen and eight lowercase alphanumerics. Some of these might have an individual .pcm.timestamp corersponding or a .pcm.lock symbolic link. And some of the folders might have a modules.idx file.

At a high level, we can say that these are pre-compiled module files and related tracking info needed to safely use and invalidate the cached compilation outputs in the face of possible underlying module changes. Digging in deeper would be wandering into compiler-internal details that are part of clang’s module implementation.

Configure message output

  --serialize-diagnostics /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/

This batch of options ensures that clang dumps as much info as it can about anything that goes wrong, and tells it to send the warnings directly to a file, rather than stderr.

Disable optimization, enable debug info


This batch of options enables debug information generation (-g),
disables optimization (-O0) so that the compiler output more closely
matches the code input to it, which make stepping by line and setting
breakpoints by line less confusing.

This also defines DEBUG to be true so app code can adapt to being compiled
with debugging enabled using code like:

    [self ensureInvariantsHaveBeenMaintained];

You might conditionally compile code based on DEBUG that is used to
perform time-consuming sanity checks on data structures
or other additional work that is useful during debugging
but is inappropriate to ship to end users.

Configure warnings


These are all warning-related flags.
The -Wno- options disable warnings; the rest enable them.
The -Werror= options cause the compiler to emit the named warnings
into errors.

Specify SDK and deployment target

  -isysroot /Applications/

-isysroot tells clang to pretend the provided directory is the root
of the filesystem, and look for system headers, libraries, and tools
relative to that folder rather than /. This is a quick shorthand to
rewrite the standard lookup paths all at once without needing a pile of
-I, -L, -F, etc. flags to do so piecemeal.

-mios-simulator-version-min tells the build system what your minimum
deployment target is.

Recall that you can build and link against a base SDK of one version
while still producing a build product that works with an earlier version;
this is the distinction made by the base SDK vs the deployment target.
Most of the time, you’ll have only whatever the latest SDK is that your
Xcode shipped with,
and you’ll target back to whatever OS version you’re supporting back to
while building against that.

Configure include and framework search paths

  -iquote /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/
  -iquote /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/

-isysroot does a lot of work for us in aiming the file lookup at the right
places, but there are still a lot of search paths specific to the app’s build
process, so that’s what this
clump of options rigs up.

There are three flavors of option used here:

  • -iquote adds a directory to the search path for headers included using
    quotes. This means #include "header.h" will look in this directory, but
    #include <header.h> won’t.
  • -I adds a directory to the overall include search path. Both the
    quote and angle bracket flavors of include will look in this directory.
  • -F adds a directory to the framework search path, which means that
    -framework Some.framework will now look in this directory for
    Some.framework, before searching the rest of the search path.

This -F flag is used to include the built-products directory. This way,
if you’re building a framework alongside your app, you can easily
link against it.

The last couple -I flags add the derived sources directory and the
architecture-specific derived sources directory to the include file search
path. This lets you generate headers during a build step and have them
picked up by the compilation build step. You can also clobber a general
header with an architecture-specific one if necessary, because the
arch-specific directory will be checked before the more general one.

The several -iquote and -I flags that lead this batch of options off
supply the path to a header map
(.hmap) file rather than to a directory.

A header map is a binary file containing a hashtable.
You could replace it with a pile of symlink files,
but the header map is more compact and supports faster header path lookup.

The header maps are used to allow including various flavor of intermediate
build files, and they all live under the $TARGET_TEMP_DIR folder.
The ones that show up here are:

  • Generated files
  • Own-target headers
  • All-target headers
  • Project headers
  • ALL the headers (no suffix, just ImportSwift.hmap)

There are actually a few more, which you can find by looking at the
$CPP_HEADERMAP_FILE_FOR family of build settings.

If you poke around
in the header-map data structure, most of these are
actually empty in this case, because this is such a simple project.
The only ones with some content are the project headers and ALL the headers.
In fact, these both contain one and the same entry,
pointing at the sole header file in the project:

Generate file dependency info

  -MT dependencies
  -MF /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/

The uppercase -M family of arguments relates to writing out dependencies
between files. The original purpose of this was autogenerating the
“if this changes, then rebuild that thing” information for Make, hence the M

-MF gives the path for the output Makefile.

-MT names the main target, which will be the first one listed in the
output Makefile, and is what a make without any arguments would try to build.

-MMD causes the compiler to dump dependencies alongside its other work,
rather than as its sole work, during the invocation. This flavor omits
emitting any dependencies on or of system headers.

This information can be used to drive incremental recompilation
as well as incremental reindexing for language-aware syntax highlighting
and code completion, though your guess is as good as mine as to what
Xcode actually does with it.

Say what to compile and to where

  -c /Users/jeremy/Workpad/BNR/ImportSwift/ImportSwift/AppDelegate.m
  -o /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/

This is the real meat of the compiler invocation; it says what file to -compile and where to -output the result.

Now you know what goes into a CompileC build step. Compiling main.m is basically the same as AppDelegate.m, only with different files, so we’re going to move along.

What’s Next

In this post, you learned exactly how the compiler compiles the source files. Next time, you’ll see how everything is pulled together.

Mark Dalrymple

Reviewer Big Nerd Ranch

MarkD is a long-time Unix and Mac developer, having worked at AOL, Google, and several start-ups over the years.  He’s the author of Advanced Mac OS X Programming: The Big Nerd Ranch Guide, over 100 blog posts for Big Nerd Ranch, and an occasional speaker at conferences. Believing in the power of community, he’s a co-founder of CocoaHeads, an international Mac and iPhone meetup, and runs the Pittsburgh PA chapter. In his spare time, he plays orchestral and swing band music.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News