Packages
mob_dev
0.3.23
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/mob.provision.ex
defmodule Mix.Tasks.Mob.Provision do
use Mix.Task
@shortdoc "Register your app ID and download an iOS provisioning profile"
@moduledoc """
Registers your app's bundle ID with Apple and downloads a development
provisioning profile so `mix mob.deploy --native` can install to a
physical iPhone.
Run this once before your first physical device deploy:
mix mob.provision
## What you need first
1. **Apple ID** — free at https://appleid.apple.com
2. **Xcode signed in** with that Apple ID:
open Xcode → Settings → Accounts → [+] → Apple ID
3. **Apple Developer Program** — optional for personal device development,
required for App Store distribution ($99/year).
Free accounts can deploy to your own devices; profiles expire every 7 days.
Paid accounts get 1-year profiles and App Store access.
Enroll at https://developer.apple.com/programs/enroll/
That's it. `mix mob.provision` handles everything else automatically.
## What it does
1. Reads your signing team from the macOS keychain or existing profiles
2. Generates `ios/Provision.xcodeproj` — a minimal Xcode project used only
for provisioning (safe to commit; useful if you ever need to open Xcode)
3. Generates `ios/MobProvision.swift` — a two-line SwiftUI stub the project
compiles to satisfy xcodebuild
4. Runs `xcodebuild -allowProvisioningUpdates` which contacts Apple to:
- Register your bundle ID in your developer account (if not registered)
- Create a development provisioning profile
- Download it to ~/Library/Developer/Xcode/.../Provisioning Profiles/
5. Verifies the profile is present
After provisioning, `mix mob.deploy --native` finds the profile automatically.
Re-run `mix mob.provision` when your profile expires.
"""
@impl Mix.Task
def run(_args) do
unless macos?() do
Mix.raise("mix mob.provision is only supported on macOS.")
end
unless File.dir?("ios") do
Mix.raise("No ios/ directory found. Run from the root of a mob iOS project.")
end
IO.puts("")
IO.puts("#{cyan()}=== iOS Provisioning ===#{reset()}")
IO.puts("")
IO.puts("#{bright()}What you need before this step:#{reset()}")
IO.puts("")
IO.puts(" 1. Apple ID — free at #{cyan()}https://appleid.apple.com#{reset()}")
IO.puts(" 2. Xcode signed in with that Apple ID")
IO.puts(" Open Xcode → Settings → Accounts → [+] → Apple ID")
IO.puts(" 3. (App Store only) Apple Developer Program — $99/year")
IO.puts(" Free accounts work for deploying to your own devices.")
IO.puts("")
IO.puts("#{bright()}Checking...#{reset()}")
IO.puts("")
check_signing_identity!()
team_id = resolve_team_id()
bundle_id = check_bundle_id!()
IO.puts("")
IO.puts(" Bundle ID : #{cyan()}#{bundle_id}#{reset()}")
IO.puts(" Team ID : #{cyan()}#{team_id}#{reset()}")
IO.puts("")
generate_xcodeproj(bundle_id, team_id)
generate_swift_stub()
IO.puts("")
IO.puts("#{bright()}Contacting Apple to register App ID and download profile...#{reset()}")
IO.puts("(requires internet — may take 10–30 seconds)")
IO.puts("")
run_xcodebuild!()
verify_profile!(bundle_id)
IO.puts("")
IO.puts("#{green()}✓ Provisioning complete!#{reset()}")
IO.puts("")
IO.puts("Next step: #{cyan()}mix mob.deploy --native#{reset()}")
IO.puts("")
IO.puts(
"#{faint()}Free Apple ID profiles expire every 7 days — re-run mix mob.provision when that happens."
)
IO.puts("Paid Developer Program profiles last 1 year.#{reset()}")
end
# ── Prerequisite checks ───────────────────────────────────────────────────────
defp check_signing_identity! do
case System.cmd("security", ["find-identity", "-v", "-p", "codesigning"],
stderr_to_stdout: true
) do
{output, 0} ->
identities =
Regex.scan(Regex.compile!("\\d+\\) [0-9A-F]+ \"([^\"]+)\""), output)
|> Enum.map(fn [_, id] -> id end)
|> Enum.filter(&String.contains?(&1, "Apple Development"))
|> Enum.uniq()
case identities do
[] ->
IO.puts(" #{red()}✗#{reset()} Apple Development certificate — not found in keychain")
Mix.raise("""
No Apple Development signing certificate found.
One-time setup:
1. Open Xcode
2. Xcode → Settings → Accounts → [+] → add your Apple ID
3. Select your team → click "Download Manual Profiles"
4. Re-run: mix mob.provision
""")
[id] ->
IO.puts(" #{green()}✓#{reset()} Signing certificate — #{faint()}#{id}#{reset()}")
many ->
IO.puts(
" #{green()}✓#{reset()} Signing certificate — #{faint()}#{hd(many)}#{reset()} (#{length(many)} found, using first)"
)
end
_ ->
Mix.raise("Could not query keychain — is this macOS?")
end
end
defp resolve_team_id do
cfg = MobDev.Config.load_mob_config()
cond do
team = cfg[:ios_team_id] ->
IO.puts(" #{green()}✓#{reset()} Team ID — #{team} #{faint()}(from mob.exs)#{reset()}")
team
team = team_from_any_profile() ->
IO.puts(
" #{green()}✓#{reset()} Team ID — #{team} #{faint()}(auto-detected from existing profile)#{reset()}"
)
team
true ->
IO.puts(" #{yellow()}?#{reset()} Team ID — could not auto-detect")
IO.puts(" Paid Apple Developer Program ($99/yr):")
IO.puts(
" #{cyan()}https://developer.apple.com/account#{reset()} → Membership → Team ID"
)
IO.puts(" Free tier (Personal Team, no $99):")
IO.puts(" Xcode → Settings → Accounts → [your Apple ID] → Team column")
team = Mix.shell().prompt(" Enter Team ID:") |> String.trim()
unless Regex.match?(Regex.compile!("^[A-Z0-9]{10}$"), team) do
Mix.raise(
"Invalid Team ID '#{team}' — expected 10 uppercase alphanumeric characters (e.g. Q89CW299G8)"
)
end
team
end
end
defp team_from_any_profile do
profile_dirs = [
Path.expand("~/Library/Developer/Xcode/UserData/Provisioning Profiles"),
Path.expand("~/Library/MobileDevice/Provisioning Profiles")
]
Enum.flat_map(profile_dirs, &Path.wildcard(Path.join(&1, "*.mobileprovision")))
|> Enum.find_value(fn path ->
with {:ok, data} <- File.read(path),
{s, _} <- :binary.match(data, "<?xml"),
{e, len} <- :binary.match(data, "</plist>") do
xml = binary_part(data, s, e - s + len)
case Regex.run(
Regex.compile!("<key>TeamIdentifier</key>\\s*<array>\\s*<string>([^<]+)</string>"),
xml
) do
[_, team] -> String.trim(team)
_ -> nil
end
else
_ -> nil
end
end)
end
defp check_bundle_id! do
bundle_id = MobDev.Config.bundle_id()
IO.puts(" #{green()}✓#{reset()} Bundle ID — #{bundle_id}")
bundle_id
end
# ── File generation ───────────────────────────────────────────────────────────
defp generate_xcodeproj(bundle_id, team_id) do
proj_dir = "ios/Provision.xcodeproj"
proj_file = Path.join(proj_dir, "project.pbxproj")
needs_write =
if File.exists?(proj_file) do
content = File.read!(proj_file)
not (String.contains?(content, bundle_id) and String.contains?(content, team_id))
else
true
end
if needs_write do
IO.puts(" Writing ios/Provision.xcodeproj...")
File.mkdir_p!(proj_dir)
File.write!(proj_file, project_pbxproj(bundle_id, team_id))
else
IO.puts(" #{green()}✓#{reset()} ios/Provision.xcodeproj — up to date")
end
end
defp generate_swift_stub do
path = "ios/MobProvision.swift"
if File.exists?(path) do
IO.puts(" #{green()}✓#{reset()} ios/MobProvision.swift — already exists")
else
IO.puts(" Writing ios/MobProvision.swift...")
File.write!(path, """
import SwiftUI
@main
struct MobProvision: App {
var body: some Scene { WindowGroup { EmptyView() } }
}
""")
end
end
# ── xcodebuild ────────────────────────────────────────────────────────────────
defp run_xcodebuild! do
args = [
"-project",
"ios/Provision.xcodeproj",
"-target",
"MobProvision",
"-destination",
"generic/platform=iOS",
"-allowProvisioningUpdates",
"-allowProvisioningDeviceRegistration",
"SYMROOT=/tmp/mob_provision_build",
"OBJROOT=/tmp/mob_provision_build",
"build"
]
{output, rc} = System.cmd("xcodebuild", args, stderr_to_stdout: true)
if rc != 0 do
# Show the full xcodebuild output first — keeps Apple's exact error
# text visible for google searches and for users comparing notes with
# online answers. The targeted hint below it is additive.
IO.puts(output)
hint = diagnose_xcodebuild_failure(output)
Mix.raise(format_xcodebuild_error(rc, hint, args))
end
# Print only the summary line on success
output
|> String.split("\n")
|> Enum.filter(&(&1 =~ Regex.compile!("^\\*\\* BUILD (SUCCEEDED|FAILED)")))
|> Enum.each(&IO.puts/1)
:ok
end
# ── xcodebuild error diagnosis ────────────────────────────────────────────
#
# Pattern-match against known Apple error strings and return a {label,
# snippet, hint} describing the targeted fix. Returns nil for unmatched
# errors — the caller falls back to a generic "common causes" message.
#
# The Apple/xcodebuild text is preserved verbatim in `:snippet` so users
# can paste it into a search engine and find existing community
# answers — our hint is additive, not a replacement.
#
# ## URL stability
#
# Each hint includes a link to Apple's official docs (`developer.apple.com/help/account/...`),
# which is Apple's account-management knowledge base — more stable than
# blog posts or Developer Forum threads. Apple does occasionally
# reorganise these; if a link 404s, search "site:developer.apple.com"
# for the section title to find its new home, then update the
# `@apple_url_*` module attributes below. The pattern matchers are the
# long-term backstop: they catch the error even when the URL goes stale.
@apple_url_app_id "https://developer.apple.com/help/account/identifiers/register-an-app-id"
@apple_url_signing_cert "https://developer.apple.com/help/account/create-certificates/create-signing-certificates"
@apple_url_team_id "https://developer.apple.com/help/account/manage-your-team/locate-your-team-id"
@doc false
@spec diagnose_xcodebuild_failure(String.t()) ::
{label :: String.t(), snippet :: String.t(), hint :: String.t()} | nil
def diagnose_xcodebuild_failure(output) do
cond do
snippet = match_invalid_app_id_name(output) ->
{"Apple rejected the auto-generated App ID display name", snippet,
"""
Apple derives the App ID display name from your bundle ID by
prepending "XC " and replacing dots with spaces. The result has
to fit Apple's portal validation (~30-char limit, no characters
their validator rejects — underscores have been flagged in some
years).
Fix: shorten the bundle ID's last segment in mob.exs:
config :mob_dev, bundle_id: "com.example.<short_name>"
Or regenerate with a shorter app name:
mix mob.new <short_name>
Apple's App ID rules: #{@apple_url_app_id}
"""}
snippet = match_no_signing_cert(output) ->
{"No Apple Development signing certificate", snippet,
"""
Open Xcode → Settings → Accounts:
1. [+] → add your Apple ID (free at https://appleid.apple.com)
2. select your team → "Manage Certificates" → "+" → Apple Development
Then re-run `mix mob.provision`.
Apple's signing certificate guide: #{@apple_url_signing_cert}
"""}
snippet = match_no_team(output) ->
{"No team available for signing", snippet,
"""
Set your Team ID in mob.exs:
config :mob_dev, ios_team_id: "ABC123XYZ4"
Find yours at:
Paid ($99/yr): https://developer.apple.com/account → Membership
Free (Personal Team): Xcode → Settings → Accounts →
[your Apple ID] → Team column
Apple's "locate your Team ID" guide: #{@apple_url_team_id}
"""}
snippet = match_app_id_quota(output) ->
{"Free-tier App ID limit (3 per 7 days) hit", snippet,
"""
Apple caps Personal Team accounts at 3 distinct bundle IDs
registered per rolling 7-day window. Either wait it out, or
reuse a bundle ID Xcode already provisioned for you by setting
it explicitly in mob.exs:
config :mob_dev, bundle_id: "com.example.<previously_registered>"
Apple's App ID registration page (mentions registration limits):
#{@apple_url_app_id}
"""}
snippet = match_bundle_id_taken(output) ->
{"Bundle ID belongs to a different team", snippet,
"""
Apple won't let two teams own the same App ID. Pick a unique
bundle ID — for personal projects, append your initials or a
random suffix:
config :mob_dev, bundle_id: "com.example.<app>.<your_suffix>"
Or change MOB_BUNDLE_PREFIX away from the conflicting reverse-DNS.
Apple's App ID rules (uniqueness across teams):
#{@apple_url_app_id}
"""}
true ->
nil
end
end
# Each match_* helper returns the verbatim snippet from xcodebuild output
# if the pattern is present, else nil. Keeping the snippet in the user's
# output (rather than rephrasing) keeps it google-searchable.
defp match_invalid_app_id_name(output) do
grep_first(output, "The attribute 'name' is invalid")
end
defp match_no_signing_cert(output) do
grep_first(output, "No signing certificate") ||
grep_first(output, "no Apple Development cert") ||
grep_first(output, "requires a development team")
end
defp match_no_team(output) do
grep_first(output, "no eligible accounts") ||
grep_first(output, "doesn't include any iOS App Development") ||
grep_first(output, "No development team")
end
defp match_app_id_quota(output) do
grep_first(output, "There are too many App IDs") ||
grep_first(output, "maximum allowed number of App IDs") ||
grep_first(output, "Maximum App IDs Reached")
end
defp match_bundle_id_taken(output) do
grep_first(output, "Failed to register bundle identifier") ||
grep_first(output, "An App ID with Identifier") ||
grep_first(output, "is not available. Please enter a different string")
end
# First line of `output` containing `needle`, or nil.
defp grep_first(output, needle) do
output
|> String.split("\n")
|> Enum.find(&String.contains?(&1, needle))
|> case do
nil -> nil
line -> String.trim(line)
end
end
defp format_xcodebuild_error(rc, nil, args) do
"""
xcodebuild provisioning failed (exit #{rc}).
Common causes:
- Xcode not signed in: open Xcode → Settings → Accounts → add Apple ID
- Bundle ID registered to a different team
- No internet connection (provisioning contacts Apple's servers)
- Free-tier Apple ID hit the 3-App-IDs-per-7-days limit
The full xcodebuild output is above; search any error line you don't
recognise — Apple's text is fairly distinctive and there's almost always
a Stack Overflow / forum hit for it.
To debug, run manually from #{File.cwd!()}:
xcodebuild #{Enum.join(args, " ")}
"""
end
defp format_xcodebuild_error(rc, {label, snippet, hint}, _args) do
"""
xcodebuild provisioning failed (exit #{rc}).
#{IO.ANSI.bright()}#{label}#{IO.ANSI.reset()}
Apple's exact error (paste this into a search engine for community answers):
#{snippet}
#{hint}
"""
end
defp verify_profile!(bundle_id) do
profile_dirs = [
Path.expand("~/Library/Developer/Xcode/UserData/Provisioning Profiles"),
Path.expand("~/Library/MobileDevice/Provisioning Profiles")
]
# Accept an exact bundle ID match or a wildcard profile (covers any bundle ID)
found =
Enum.flat_map(profile_dirs, &Path.wildcard(Path.join(&1, "*.mobileprovision")))
|> Enum.any?(fn path ->
case File.read(path) do
{:ok, data} ->
String.contains?(data, bundle_id) or
Regex.match?(
Regex.compile!(
"<key>application-identifier</key>\\s*<string>[^<]+\\.\\*</string>"
),
data
)
_ ->
false
end
end)
if found do
IO.puts(" #{green()}✓#{reset()} Provisioning profile ready")
else
IO.puts(
" #{yellow()}⚠#{reset()} Profile not found — re-run `mix mob.provision` if deploy fails"
)
end
end
# ── project.pbxproj template ──────────────────────────────────────────────────
#
# A minimal Xcode project with a single Swift target. The UUIDs are fixed (they
# only need to be unique within this file). MobProvision.swift is referenced
# relative to the ios/ directory (the directory containing Provision.xcodeproj).
defp project_pbxproj(bundle_id, team_id) do
"""
// !$*UTF8*$!
{
\tarchiveVersion = 1;
\tclasses = {
\t};
\tobjectVersion = 77;
\tobjects = {
/* Begin PBXBuildFile section */
\t\tAA000001 /* MobProvision.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002 /* MobProvision.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
\t\tAA000002 /* MobProvision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobProvision.swift; sourceTree = "<group>"; };
\t\tAA000003 /* MobProvision.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MobProvision.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
\t\tAA000004 = {
\t\t\tisa = PBXGroup;
\t\t\tchildren = (
\t\t\t\tAA000002 /* MobProvision.swift */,
\t\t\t\tAA000005 /* Products */,
\t\t\t);
\t\t\tsourceTree = "<group>";
\t\t};
\t\tAA000005 /* Products */ = {
\t\t\tisa = PBXGroup;
\t\t\tchildren = (
\t\t\t\tAA000003 /* MobProvision.app */,
\t\t\t);
\t\t\tname = Products;
\t\t\tsourceTree = "<group>";
\t\t};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
\t\tAA000006 /* MobProvision */ = {
\t\t\tisa = PBXNativeTarget;
\t\t\tbuildConfigurationList = AA000007 /* Build configuration list for PBXNativeTarget "MobProvision" */;
\t\t\tbuildPhases = (
\t\t\t\tAA000008 /* Sources */,
\t\t\t);
\t\t\tbuildRules = (
\t\t\t);
\t\t\tdependencies = (
\t\t\t);
\t\t\tname = MobProvision;
\t\t\tproductName = MobProvision;
\t\t\tproductReference = AA000003 /* MobProvision.app */;
\t\t\tproductType = "com.apple.product-type.application";
\t\t};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
\t\tAA000009 /* Project object */ = {
\t\t\tisa = PBXProject;
\t\t\tattributes = {
\t\t\t\tBuildIndependentTargetsInParallel = YES;
\t\t\t\tLastUpgradeCheck = 1600;
\t\t\t};
\t\t\tbuildConfigurationList = AA00000A /* Build configuration list for PBXProject "Provision" */;
\t\t\tdevelopmentRegion = en;
\t\t\thasScannedForEncodings = 0;
\t\t\tknownRegions = (
\t\t\t\tBase,
\t\t\t\ten,
\t\t\t);
\t\t\tmainGroup = AA000004;
\t\t\tproductRefGroup = AA000005 /* Products */;
\t\t\tprojectDirPath = "";
\t\t\tprojectRoot = "";
\t\t\ttargets = (
\t\t\t\tAA000006 /* MobProvision */,
\t\t\t);
\t\t};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
\t\tAA000008 /* Sources */ = {
\t\t\tisa = PBXSourcesBuildPhase;
\t\t\tbuildActionMask = 2147483647;
\t\t\tfiles = (
\t\t\t\tAA000001 /* MobProvision.swift in Sources */,
\t\t\t);
\t\t\trunOnlyForDeploymentPostprocessing = 0;
\t\t};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
\t\tAA00000B /* Debug */ = {
\t\t\tisa = XCBuildConfiguration;
\t\t\tbuildSettings = {
\t\t\t\tCODE_SIGN_STYLE = Automatic;
\t\t\t\tDEVELOPMENT_TEAM = #{team_id};
\t\t\t\tGENERATE_INFOPLIST_FILE = YES;
\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;
\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0;
\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = #{bundle_id};
\t\t\t\tPRODUCT_NAME = MobProvision;
\t\t\t\tSDKROOT = iphoneos;
\t\t\t\tSWIFT_VERSION = 5.9;
\t\t\t\tTARGETED_DEVICE_FAMILY = "1,2";
\t\t\t};
\t\t\tname = Debug;
\t\t};
\t\tAA00000C /* Release */ = {
\t\t\tisa = XCBuildConfiguration;
\t\t\tbuildSettings = {
\t\t\t\tCODE_SIGN_STYLE = Automatic;
\t\t\t\tDEVELOPMENT_TEAM = #{team_id};
\t\t\t\tGENERATE_INFOPLIST_FILE = YES;
\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;
\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0;
\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = #{bundle_id};
\t\t\t\tPRODUCT_NAME = MobProvision;
\t\t\t\tSDKROOT = iphoneos;
\t\t\t\tSWIFT_VERSION = 5.9;
\t\t\t\tTARGETED_DEVICE_FAMILY = "1,2";
\t\t\t};
\t\t\tname = Release;
\t\t};
\t\tAA00000D /* Debug */ = {
\t\t\tisa = XCBuildConfiguration;
\t\t\tbuildSettings = {
\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;
\t\t\t\tSDKROOT = iphoneos;
\t\t\t};
\t\t\tname = Debug;
\t\t};
\t\tAA00000E /* Release */ = {
\t\t\tisa = XCBuildConfiguration;
\t\t\tbuildSettings = {
\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;
\t\t\t\tSDKROOT = iphoneos;
\t\t\t};
\t\t\tname = Release;
\t\t};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
\t\tAA000007 /* Build configuration list for PBXNativeTarget "MobProvision" */ = {
\t\t\tisa = XCConfigurationList;
\t\t\tbuildConfigurations = (
\t\t\t\tAA00000B /* Debug */,
\t\t\t\tAA00000C /* Release */,
\t\t\t);
\t\t\tdefaultConfigurationIsVisible = 0;
\t\t\tdefaultConfigurationName = Debug;
\t\t};
\t\tAA00000A /* Build configuration list for PBXProject "Provision" */ = {
\t\t\tisa = XCConfigurationList;
\t\t\tbuildConfigurations = (
\t\t\t\tAA00000D /* Debug */,
\t\t\t\tAA00000E /* Release */,
\t\t\t);
\t\t\tdefaultConfigurationIsVisible = 0;
\t\t\tdefaultConfigurationName = Debug;
\t\t};
/* End XCConfigurationList section */
\t};
\trootObject = AA000009 /* Project object */;
}
"""
end
# ── ANSI helpers ──────────────────────────────────────────────────────────────
defp macos?, do: match?({:unix, :darwin}, :os.type())
defp green, do: IO.ANSI.green()
defp yellow, do: IO.ANSI.yellow()
defp red, do: IO.ANSI.red()
defp cyan, do: IO.ANSI.cyan()
defp bright, do: IO.ANSI.bright()
defp faint, do: IO.ANSI.faint()
defp reset, do: IO.ANSI.reset()
end