Build và phân phối ứng dụng Mac·iOS mà không cần mở Xcode
(scottwillsey.com)- Sau khi hoàn tất thiết lập ban đầu cho tài khoản Apple và chứng chỉ, có thể tự động hóa việc build, ký và phân phối ứng dụng Mac và iOS chỉ bằng các công cụ CLI bên trong Xcode.app
- Tái tạo
.xcodeprojtừproject.ymlbằng XcodeGen, rồi xử lý từ tạo project đến công chứng Mac và cài đặt lên iPhone bằngxcodebuild,notarytool,stapler,devicectl - Phân phối trên Mac được thực hiện tuần tự chỉ với một
scripts/release.sh: archive, ký Developer ID, công chứng, stapling ticket, xác minh Gatekeeper, cài vào/Applications, và dừng ngay nếu có lỗi - Có thể dùng
CODE_SIGNING_ALLOWED=NOđể biên dịch và test nhanh, nhưng bản build ký tạm thời này không vượt qua Gatekeeper, đồng thời quyền iCloud KVS và App Group cũng không được gắn với team ID thực tế - Lưu khóa riêng để ký và mật khẩu công chứng trong login keychain, đồng thời ghi lại lệnh và quy tắc phân phối trong
CLAUDE.mdhoặcAGENTS.mdđể các agent như Claude Code có thể lặp lại toàn bộ workflow
Phạm vi có thể xử lý mà không cần GUI Xcode
- Vẫn cần cài Xcode, nhưng không cần chạy;
xcodebuild,notarytool,stapler,devicectlđều là công cụ shell bên trong Xcode.app và có thể chạy độc lập - Các thiết lập lần đầu như đăng nhập Apple ID, tạo chứng chỉ Developer ID, lưu mật khẩu công chứng cần GUI hoặc terminal tương tác
- Sau khi hoàn tất thiết lập ban đầu, có thể xử lý không cần chuột: tạo project, build, archive, xuất app đã ký, công chứng, xác minh, cài đặt, và phân phối lên iPhone thật
- Khi gặp vướng mắc trong thiết lập hoặc viết script, có thể cung cấp yêu cầu công việc và các giá trị theo từng project cho Claude Code hoặc công cụ lập trình LLM khác để tạo workflow
Chọn toàn bộ toolchain Xcode
- Trước tiên cần kiểm tra kết quả của
xcode-select -pcó phải là/Applications/Xcode.app/Contents/Developerhay không - Nếu đang chọn Command Line Tools độc lập, hãy chỉ định toàn bộ toolchain Xcode bằng lệnh sau
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
/Library/Developer/CommandLineToolsđược cài bằngxcode-select --installcóclangvàgit, nhưng không có bộ công cụ phát triển app đầy đủ như iOS SDK,notarytool,devicectl- Nếu Xcode đã được cài sẵn, không cần gói Command Line Tools riêng
- Việc chấp nhận license và cài đặt các thành phần bổ sung cũng có thể xử lý bằng CLI
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
Tái tạo project bằng XcodeGen
- XcodeGen quản lý cấu hình project Xcode bằng YAML và tạo lại
.xcodeprojmỗi lần build - Có thể tải từ GitHub hoặc cài bằng Homebrew
brew install xcodegen
.xcodeprojlà một thư mục trông như file trên macOS; vì Xcode liên tục thay đổi các file và tham chiếu bên trong, có thể phát sinh conflict hoặc thay đổi không cần thiết trong Git repository- Nếu chỉ commit
project.ymlvào Git và bỏ qua.xcodeprojđược tạo ra, có thể giữ cấu hình project ở trạng thái tái lập được
xcodegen generate
Thiết lập tài khoản nhà phát triển Apple và chứng chỉ
- Cần thêm Apple ID trong
Settings → Accountscủa Xcode - Việc phân phối và công chứng app cần tài khoản Apple Developer trả phí
- Tạo chứng chỉ phân phối tại
Settings → Accounts → Apple ID → Manage Certificates… → + → Developer ID Application - Apple Development và Developer ID Application có mục đích khác nhau
- Apple Development dùng để chạy trên thiết bị của bạn hoặc chuyển sang iPhone để debug cục bộ
- Developer ID Application cần cho việc phân phối
.appđã công chứng, vượt qua Gatekeeper trên Mac của người khác
- Khi tạo chứng chỉ Developer ID Application, chứng chỉ và khóa riêng tương ứng được cài vào login keychain
- Việc ký thực tế dùng khóa riêng, và khóa này không thể tải lại, vì vậy đừng xóa và cần sao lưu keychain
Lưu thông tin xác thực công chứng vào keychain
- Công chứng app Mac là quá trình upload app đã ký lên Apple để kiểm tra mã độc
notarytoolxác thực bằng profile lưu trong keychain; khi tạo profile lần đầu, cần nhập mật khẩu dành riêng cho app theo cách tương tác
xcrun notarytool store-credentials App-Name \
--apple-id "you@example.com" \
--team-id YOUR-TEAM-ID
- Không nên mượn profile công chứng của app khác; cần tạo theo tên app để tránh tình huống thất bại âm thầm ở môi trường khác
- Mật khẩu dành riêng cho app khác với mật khẩu Apple ID và được tạo tại
Sign-In & Security → App-Specific Passwordstrên trang Apple ID - Nếu đổi mật khẩu Apple ID, mật khẩu dành riêng cho app có thể hết hạn mà không có cảnh báo riêng
- Nếu gặp
401 invalid credentialstrong lúc công chứng, trước khi nghi ngờ toàn bộ thiết lập, hãy kiểm tra việc mật khẩu dành riêng cho app đã hết hạn
- Nếu gặp
- Kiểm tra profile đã được lưu hay chưa bằng lệnh sau
xcrun notarytool history --keychain-profile App-Name
- Nếu lưu mật khẩu dành riêng cho app trong vault 1Password mà Claude Code có thể truy cập, bạn cũng có thể giao cho agent tạo profile công chứng cho app mới
- Có thể tự động hóa cả việc nhập mật khẩu bằng
--password, nhưng mật khẩu có thể lưu lại trong lịch sử shell, nên hãy nhập thủ công một lần rồi lưu trong keychain hoặc 1Password
Tách cấu hình ký cục bộ và thông tin bí mật
- Team ID và bundle prefix cần cho việc ký thực tế được lưu trong
Local.xcconfig
cp Local.xcconfig.example Local.xcconfig
BUNDLE_PREFIX = your.real.prefix
DEVELOPMENT_TEAM = YOUR-TEAM-ID
- Thêm
Local.xcconfigvào.gitignoređể cấu hình cục bộ theo từng project không lọt vào repository - Khóa riêng để ký nằm trong login keychain, mật khẩu dành riêng cho app để công chứng nằm trong profile keychain của
notarytool, vì vậy thông tin bí mật không được ghi vào Git
Tự động hóa phân phối Mac bằng release.sh
scripts/release.shtrong repository chạy toàn bộ pipeline phân phối app Mac bằng một lệnh
./scripts/release.sh
-
Script hoạt động theo thứ tự sau
- Kiểm tra trước xem
xcodegenvà profile công chứng có tồn tại hay không - Tái tạo project bằng
xcodegen generate - Tạo archive Release bằng
xcodebuild archive - Xuất app ký Developer ID bằng
ExportOptions.plistvàxcodebuild -exportArchive - Nén app thành ZIP và gửi lên Apple bằng
notarytool submit --wait - Gắn ticket công chứng bằng
stapler staple - Xác minh Gatekeeper có cho phép bằng
spctl - Kết thúc process hiện có và sao chép app vào
/Applications - Đăng ký app đã cài bằng
lsregister - Xác minh lại ticket và trạng thái Gatekeeper của bundle đã cài
- Kiểm tra trước xem
-
Dù model có biết quy trình phân phối chung, người dùng vẫn cần cung cấp thông tin theo từng project như tên scheme, team ID, tên profile công chứng, vị trí cài đặt
-
Nếu script đầu tiên thất bại, cần lặp lại việc sửa dựa trên kết quả chạy; nhưng sau khi workflow ổn định, chỉ cần chạy lệnh phân phối mà không phải sửa thêm
-
Ngăn lỗi và phân phối không hoàn chỉnh
set -euo pipefailkhiến script dừng ngay nếu bất kỳ lệnh nào thất bại, tránh coi trạng thái không hoàn chỉnh là thành côngcd "$(dirname "$0")/.."chuyển về root của repository dù script được gọi từ thư mục nào- Khối kiểm tra trước xác minh
xcodegenvà profile công chứng trước bước archive tốn thời gian - Kiểm tra lại không chỉ app đã xuất mà cả app đã sao chép vào
/Applicationsđể phát hiện trường hợp bundle bị hỏng trong quá trình sao chép - Nếu cần profile công chứng khác, có thể ghi đè bằng biến môi trường
TZED_NOTARY_PROFILE=<name> ./scripts/release.sh
Truyền quy tắc phân phối cho agent
- Nếu
release.shthực hiện phân phối bằng một lệnh,CLAUDE.mdhoặcAGENTS.mdsẽ giúp agent dùng lệnh đó trong mỗi phiên mà không cần chỉ dẫn riêng - Tài liệu ghi lại việc tái tạo project, unit test, build nhanh cho macOS và lệnh phân phối thực tế
xcodegen generate
swift test
xcodebuild -project YOUR-APP-NAME.xcodeproj \
-scheme YOUR-APP-NAME-macOS \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO build
./scripts/release.sh
- Cũng cần nêu rõ khác biệt giữa hai đường build trong tài liệu
- Build
CODE_SIGNING_ALLOWED=NOphù hợp cho CI và kiểm tra cục bộ nhanh - Việc phân phối app menu bar thực tế, vượt qua thuộc tính quarantine và đồng bộ iCloud cần
release.sh, vốn thực hiện ký Developer ID và công chứng
- Build
- Nếu chỉ dẫn tham chiếu cách làm từ một app sang repository khác, Claude Code có thể sao chép cùng thiết lập cho các app về sau
Build và test nhanh không ký
- Unit test dựa trên Swift Package Manager chạy không cần build bằng Xcode
swift test
- Kiểm tra biên dịch nhanh app macOS được thực hiện khi tắt ký
xcodebuild -project TZed.xcodeproj \
-scheme TZed-macOS \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO build
- Build simulator cho app iOS và widget extension cũng có thể chạy theo cùng cách
xcodebuild -project TZed.xcodeproj \
-scheme TZed-iOS \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO build
- Bản build ký tạm thời tạo bằng
CODE_SIGNING_ALLOWED=NOcó thể dùng để biên dịch và chạy trên simulator - Bản build này bị Gatekeeper từ chối, và do không có team prefix nên các quyền như iCloud KVS và App Group không được gắn, vì vậy không thể dùng để phân phối
Phân phối headless lên iPhone thật
- Phân phối lên thiết bị iOS không có bước công chứng như app Mac, và dùng
xcodebuildcùngdevicectl - App cho thiết bị thật được build và ký bằng chứng chỉ Apple Development và development provisioning profile
xcodebuild -project TZed.xcodeproj \
-scheme TZed-iOS \
-destination 'generic/platform=iOS' \
-allowProvisioningUpdates \
-derivedDataPath build/ios \
archive -archivePath build/TZed-iOS.xcarchive
.appđược tạo ra sẽ được cài bằng cách chỉ định UDID của thiết bị đã kết nối
xcrun devicectl device install app \
--device <DEVICE-UDID> build/ios/…/TZed.app
devicectl list deviceshiển thị các thiết bị đã kết nối và ghép đôi cùng UDID- Chỉ định
-allowProvisioningUpdatessẽ lấy development provisioning profile cần thiết
Cách code signing hoạt động khi không có GUI
-
Khi tạo chứng chỉ Developer ID Application, Apple phát hành chứng chỉ, còn Mac tạo khóa riêng tương ứng và lưu cả hai vào login keychain
-
codesigndoxcodebuildgọi sẽ ký binary bằng khóa riêng, đồng thời đưa chứng chỉ dẫn tới Apple root certificate vào app để có thể xác minh người ký -
Khi dùng
signingStyle: automatic,xcodebuildchọn chứng chỉ phù hợp với team ID và lấy provisioning profile cần thiết từ Apple -
Vì vậy không cần commit provisioning profile vào repository
-
Entitlements, công chứng và stapling
- File
.entitlementscủa từng target chứa các quyền như sandbox, network client, iCloud KVS, App Group - Entitlements được gắn khi ký bằng team ID thực tế, nên trong bản build ký tạm thời, iCloud và App Group không hoạt động bình thường
- Ký và công chứng là hai bước khác nhau
- Ký chứng minh ai đã tạo app
- Công chứng là quy trình Apple kiểm tra mã độc trên app đã ký rồi cấp ticket
- Stapling gắn ticket đó vào app để Gatekeeper vẫn tin cậy kể cả khi offline
- Với app
LSUIElementlà app menu bar dùng UI ẩn, công chứng cần thiết để tránh bị XProtect phát hiện
- File
-
Lệnh xác minh thủ công
codesign -dv --verbose=4 /Applications/TZed.app spctl -a -vvv -t exec /Applications/TZed.app stapler validate /Applications/TZed.appcodesignkiểm tra người ký và chứng chỉspctlkiểm tra Gatekeeper có cho phép chạy hay khôngstapler validatexác minh ticket công chứng đã được gắn
Công cụ chuẩn mà agent chạy
- Không cần MCP server riêng cho build hoặc plugin chuyên dụng; Claude Code chạy CLI chuẩn trong shell không tương tác
- Các công cụ được dùng là
xcodebuild,xcrun notarytool,xcrun stapler,spctl,codesign,devicectl,xcodegen,swift CLAUDE.mdtruyền cho agent quy tắc đặt tên profile công chứng, sự khác biệt giữa build nhanh và phân phối thật, và quy tắc phải dùngrelease.shkhi phân phối- Chỉ còn
notarytool store-credentialslần đầu là bước tương tác; toàn bộ quá trình sau đó có thể tự động hóa
Thay thế thao tác GUI Xcode bằng CLI
- Việc tạo
.xcodeprojdo Xcode quản lý được thay bằngproject.ymlvàxcodegen generate ⌘Bhoặc nút Run tương ứng vớixcodebuild … buildProduct → Archiveđược xử lý bằngxcodebuild … archive- Việc phân phối app trong Organizer có thể thay bằng
xcodebuild -exportArchive - Upload từ Organizer được thực hiện bằng
xcrun notarytool submit --wait - Dùng
xcrun stapler stapleđể tự động gắn ticket - Quá trình cài bằng cách kéo thả vào
/Applicationsđược xử lý bằngcp -Rvàlsregister - Thao tác Run trên iPhone thật được thay bằng
xcodebuild archivevàdevicectl device install - Sau khi cài Xcode và XcodeGen, thiết lập thông tin xác thực ban đầu, viết xong
release.shvàCLAUDE.md, việc phân phối build mới về sau có thể chạy bằng một câu hoặc một lệnh
1 bình luận
Ý kiến trên Hacker News
Có quá nhiều việc không thể giải quyết trong sandbox nên tôi chạy AI agent trực tiếp trên máy Mac, nhưng sau khi thấy vụ xAI tải lên cả thư mục home bao gồm SSH key, tôi bắt đầu nghiêm túc cân nhắc lại lựa chọn này
Việc bản thân máy bị hỏng không phải điều tôi quá lo. Hầu hết đều đã được sao lưu hoặc commit lên Git nên chỉ cần vài giờ là có thể khôi phục. Nhưng tôi tuyệt đối không muốn thư mục
.sshbị gửi cho AI agent, và chuyện này không nên bị chặn bằng prompt mà phải bị chặn ở cấp hệ thống. Tôi đang cân nhắc tạo một user riêng và áp dụng quyền700cho thư mục home. Bảo mật có vẻ như đã thụt lùi về thập niên 1990, nhưng đổi lại có thể hoàn thành công việc nhanh chưa từng thấy. Tôi cũng đang dùng cách làm đầy rủi ro này nên không có ý chỉ trích người khácCó thể dùng các giải pháp đóng gói như Tart mã nguồn mở https://tart.run/ hoặc VirtualBuddy https://github.com/insidegui/VirtualBuddy, hoặc tự vibe coding một công cụ bọc quanh OS API https://developer.apple.com/documentation/virtualization. Gần đây còn có cả Apple container https://github.com/apple/container
agent. Một năm trước tôi thất bại vì OAuth và keychain, nhưng gần đây đã triển khai được nhờ hướng dẫn từ Claude rằng có thể dùngCLAUDE_CODE_OAUTH_TOKENCần thêm một công cụ phụ để thiết lập quyền bằng ACL, nhưng hiện tại nó đã chạy được trên macOS. Mức độ rủi ro nằm đâu đó giữa sandbox mặc định của Claude và cách cô lập hoàn toàn bằng container hoặc máy riêng. Khi sandbox của Claude được cải thiện, tôi từng nghi ngờ hiệu quả đầu tư của dự án phòng thủ hai lớp này, nhưng nhìn vào khả năng rò rỉ thư mục home thì vẫn còn quá đủ lý do để tiếp tục. Tôi chưa công khai mã nguồn mở, nhưng sẵn sàng trao đổi với ai quan tâm
Tôi đang build và test ứng dụng iOS chỉ bằng Linux cho vui, và mọi thứ đơn giản đến đáng ngạc nhiên, https://github.com/xtool-org/xtool hoạt động rất tốt
Ngay cả trên Linux cũng có thể cài trực tiếp lên iPhone qua USB mà không cần đưa lên TestFlight hay App Store. Nếu chưa chắc, bạn có thể bắt đầu dễ dàng bằng cách nhờ coding agent tạo rồi upload một ứng dụng Hello World cho iOS
Dự án mã nguồn mở Axiom của tôi giúp các công cụ thực thi code làm tốt công việc phát triển cho hệ điều hành Apple
Nó không chỉ hỗ trợ nhiều công nghệ và agent khác nhau mà còn bao gồm các công cụ cho LLM như
xclog,xcprof,xcsym,xcui. Chúng phơi bày các chức năng cần thiết theo cách hiệu quả về token và cũng hữu ích cho các công nghệ hoặc agent ngoài Axiom. Axiom: https://charleswiltgen.github.io/Axiom/, công cụ CLI: https://charleswiltgen.github.io/Axiom/tools/Đọc câu kiểu “Tôi đã bảo Claude Code tạo một script để archive mà không mở Xcode, ký bằng Developer ID, notarize, staple, cài vào
/Applications, và dừng rõ ràng ở bất kỳ bước nào nếu thất bại” thấy thật kỳ lạ. Vì ngay cả bài viết tôi đang đọc cũng giống như Claude đang nói chuyện với chúng ta vậyCũng thật lạ khi ở gần như mọi bước trong bài đều lặp lại những hướng dẫn như “hãy cho Claude Code hoặc công cụ coding LLM bạn muốn dùng xem bài này rồi để nó tự xử lý”, “nếu không biết thì hãy hỏi LLM để nó giúp thiết lập”, “mục đích dùng LLM là để tránh công việc thủ công không mong muốn”, “hãy bảo nó tạo workflow”, tức là liên tục bảo người đọc quay lại hỏi LLM
Với góc nhìn của một người đã phát triển ứng dụng Mac và iOS bằng agent hơn một năm, tôi cho rằng bài này là lời khuyên không tốt. Đặc biệt, Xcode MCP xuất sắc yêu cầu Xcode phải đang chạy
Khi dùng công cụ MCP, có thể truy cập nhiều chức năng nhanh và gọn hơn rất nhiều so với
xcodebuild, đồng thời còn có thể tạo và render#Preview, điều không thể làm được nếu đóng Xcode. MCP và agent tích hợp của Xcode 27 cũng điều khiển simulator rất tốt thông qua DeviceHub. DeviceHub thay thế Simulator.app, và có vẻ agent có thể hiểu và xử lý mà không cần lách để thao tác accessibility API bằng các công cụ nhưaxeXcode MCP không phải hoàn hảo. Vấn đề là mỗi lần chạy agent đều hiện cửa sổ xin quyền, nên tôi đã lách bằng cách cho Keyboard Maestro tự động chấp thuận. Dù vậy, Xcode 27 đã cải thiện lớn tính khả dụng của vibe coding trong Xcode. Giờ tôi chạy Codex ngay trong UI agent của Xcode thay vì phải tự xử lý MCP riêng, và môi trường chạy này dẫn dắt model tốt hơn để tận dụng các công cụ có sẵn. Cuối cùng, điều quan trọng nhất là có được phản hồi nhanh hơn và kiểm chứng vững chắc hơn
Xcode-mcp-proxyvừa làm proxy cho Xcode MCP vừa bấm hộ các hộp thoại và khôi phục kết nối. Tôi tò mò không biết khi test ứng dụng macOS thì bạn dùng Peekaboo hay chỉ dùng tự động hóa screenshot thông thườngNếu muốn hạn chế tối đa việc đụng vào IDE của iOS và Android thì React Native và Expo cũng là lựa chọn tốt
Chỉ dùng Xcode và Android Studio khi cài device simulator, còn lại đến cả khâu phân phối lên store thì toolchain của Expo xử lý. Ngay cả trên Windows không thể cài Xcode, nếu dùng ứng dụng managed của Expo thì vẫn có thể chạy trực tiếp app iOS trên iPhone. Làm điểm khởi đầu dựa trên RN và Expo thì Ignite được bảo trì rất tốt: https://github.com/infinitered/ignite
Nói chính xác thì đây vẫn là dùng Xcode. Xcode về cơ bản là một GUI rất nhiều bug bọc quanh nhiều tiện ích và ứng dụng UNIX cấp hệ thống khá ổn định
Cách phát hành app bằng CLI là thông lệ từ lâu, ít nhất là đã làm được từ năm 2012 khi tôi phát hành ứng dụng Xcode đầu tiên
Ít nhất với một phần debugging thì ai cũng buộc phải dùng Xcode
Việc build thì agent đã có thể xử lý phần lớn từ dòng lệnh, và nếu đưa chỉ dẫn rõ ràng theo từng tình huống thì còn có thể build nhanh hơn. Nếu dùng nhóm đồng bộ hóa file system của Xcode thì XcodeGen có thể chỉ là phần việc phát sinh không cần thiết
Trên iOS, điều tôi muốn khuyên nhất là thêm chức năng App Store Connect cho agent: https://github.com/rorkai/App-Store-Connect-CLI. Làm vậy không chỉ không cần luôn mở Xcode mà còn không cần phải ở gần MacBook. Bạn có thể sửa bằng Codex trên iPhone, rồi bảo
ascbuild và upload lên TestFlight, tải bản mới xuống chạy và lặp lạiTôi vẫn luôn bận tâm về thực tế là các lập trình viên không đủ ngân sách mua Mac gần như bị loại khỏi việc phát triển iOS. Mỗi lần giá sản phẩm Apple tăng lên thì lại có thêm người không thể tham gia phát triển
Nếu vibe coding ứng dụng thì về lâu dài Expo là lựa chọn tốt hơn. Vì nó dựa trên React nên dữ liệu huấn luyện LLM có mã React nhiều hơn Swift rất nhiều, và chất lượng mã tạo ra cũng cao hơn
Có thể làm mọi thứ từ dòng lệnh và vì là web view nên cũng dễ debug. Sau khi compile thành ứng dụng iOS native thì cảm giác sử dụng cũng tương tự các app native khác. Kết hợp Expo và Fastlane thì có thể tự động hóa hoàn toàn việc submit và phân phối iOS, để chỉ với một lệnh là có thể đưa bản mới lên App Store