Giả sử bạn đã xây dựng một ứng dụng danh sách việc cần làm trong chat suốt vài tuần qua. Hiện nó đang là một trang web — React, một cơ sở dữ liệu, không có gì cầu kỳ. Bạn gõ "làm cho tôi một phiên bản Android" và nhấn enter. Đây là những gì thực sự xảy ra giữa lần gõ phím đó và một file .aab xuất hiện trong thư mục tải xuống của bạn, vì hầu hết các nền tảng không cho bạn thấy phần này, và phần bị che giấu đó chính là nơi từng chứa mọi nỗi đau.
File .aab của Android
Gradle tiếp quản trước tiên. Các module gốc (native) của ứng dụng bạn — quyền truy cập camera, lưu trữ cục bộ, hay bất kỳ plugin nào được build kéo vào — mỗi cái đều khai báo phiên bản NDK mà chúng được biên dịch cùng, và các khai báo đó không phải lúc nào cũng khớp nhau. Tôi từng thấy một module được build với NDK r25 từ chối liên kết với một module khác giả định dùng r26, và lỗi nó ném ra không nói "phiên bản không khớp", mà lại là một thứ gì đó về ký hiệu (symbol) bị thiếu ở tận ba lớp sâu trong một file .so. Các phiên bản Kotlin còn tinh vi hơn: một phiên bản được ghim bên trong một module Gradle có thể âm thầm che khuất phiên bản được khai báo ở đầu build script của bạn, và quá trình build vẫn thành công — chỉ là nó tạo ra một file binary bị crash trên một số phiên bản Android cụ thể khi triển khai thực tế. Không có gì trong số này là hiếm gặp cả. Đó là chi phí tiêu chuẩn khi phát hành ứng dụng Android gốc, và đó là lý do các đội ngũ phải thuê một người có công việc duy nhất là biết cờ (flag) nào sẽ khiến lỗi tuần này biến mất.
Quá trình build ở đây chạy toolchain thực thụ và tự mình xử lý công việc phân giải đó:
- Xung đột phụ thuộc được phát hiện trước khi trở thành lỗi crash lúc chạy
- Cấu hình toolchain ngày càng cải thiện mỗi khi có một lỗi mới dạy nó thêm điều gì đó
Một phiên bản giả nhanh hơn của việc này — thứ tạo hình một file .aab mà không thực sự chạy các tác vụ Gradle — sẽ hoàn thành trong chưa đầy một phút. Nhưng nó cũng sẽ chết ngay khi ứng dụng của bạn cần một dịch vụ chạy nền hoặc một thư viện mã hóa gốc, và việc kiểm duyệt trên Play Store sẽ gắn cờ nó trong vòng một ngày. Chúng tôi thà bỏ ra hai mươi phút còn hơn.
File .dmg của macOS
Hai bản build được gộp vào file này. Công cụ dòng lệnh của Xcode biên dịch riêng một file binary cho Apple Silicon và một file binary cho Intel, sau đó lipo gắn kết chúng thành một file thực thi phổ quát duy nhất.
Điều đó nghe hấp dẫn — chỉ phát hành một file binary duy nhất rồi coi như xong — cho đến khi bạn nhớ ra rằng rất nhiều người dùng laptop của công ty, chứ không phải phần cứng họ tự chọn, và chiếc laptop đó có thể đã ba năm tuổi và chạy chip Intel. Thay vì bắt người dùng phải tự xác định họ đang dùng chip gì (hầu hết không biết), chúng tôi phát hành cả hai và để hệ điều hành tự âm thầm chọn. Giải pháp thay thế mà chúng tôi từng thử ở giai đoạn đầu là biên dịch chéo mọi thứ từ một máy Linux bằng các toolchain giả lập. Cách đó nhanh hơn. Nhưng đó cũng chính là cách bạn gặp phải một trường hợp lỗi ký số (codesigning) hy hữu chỉ xuất hiện trên phần cứng macOS 12 thật, sáu tuần sau khi phát hành, được báo cáo bởi một người dùng bối rối không hiểu vì sao ứng dụng của họ không mở được.
Trình cài đặt Windows
Đây chính là lúc trải nghiệm lần chạy đầu tiên quyết định liệu người dùng của bạn có tin tưởng ứng dụng hay không. Windows SmartScreen chưa biết đến trình cài đặt của bạn — nó chưa tích lũy được uy tín với máy chủ của Microsoft — nên nó hiển thị màn hình màu xanh "Windows đã bảo vệ PC của bạn" với một nút ghi "Không chạy" in đậm và một liên kết "Thông tin thêm" gần như vô hình, mà khi nhấp vào sẽ hiện ra "Vẫn chạy". macOS cũng có màn trình diễn tương tự của riêng nó: nhấp chuột phải, chọn Mở, xác nhận, vì các ứng dụng bên ngoài App Store cũng không mặc định được tin tưởng. Ban đầu chúng tôi dẫn cả hai trường hợp này đến một trang FAQ chung chung. Các ticket hỗ trợ cho chúng tôi thấy cách đó không hiệu quả — một người đang nhìn chằm chằm vào màn hình nói rằng file tải về của họ có thể là mã độc sẽ không đi đọc tài liệu, mà chụp màn hình lại và hỏi liệu họ có bị hack không. Vì vậy luồng cài đặt sẽ tự phát hiện hệ điều hành và hiển thị đúng ba cú nhấp cần thiết, không cần đến FAQ. Chi tiết nhỏ thôi, nhưng bản thân file cũng quan trọng: file tải về được đặt tên theo sản phẩm của bạn, chứ không phải theo tên tệp build. Không ai nên phải giải thích qua chat hỗ trợ rằng họ đã tải về "app-release-signed-v2-final.exe" và không biết đó có phải là file đúng hay không.
Manifest của tiện ích mở rộng trình duyệt
Đây là trường hợp khác biệt trong toàn bộ pipeline — không có Gradle, không có NDK, không có bước biên dịch theo nghĩa thông thường. Thay vào đó là một manifest, và manifest chính là một cuộc thương lượng với một người kiểm duyệt Chrome Web Store mà bạn sẽ không bao giờ nói chuyện trực tiếp.
| Quyền được yêu cầu | Kết quả kiểm duyệt |
|---|---|
| <all_urls> (rộng hơn mức tính năng cần) | Hai tuần qua lại với một người không nói rõ họ phản đối điều gì |
| activeTab (giới hạn đúng theo nhu cầu thực tế) | Được duyệt ngay trong ngày |
MV3 cũng làm phức tạp thêm một thứ mà MV2 từng làm dễ dàng: các service worker chạy nền sẽ bị hủy tải giữa chừng theo thiết kế, một quyết định chính sách của Google nhằm tiết kiệm pin, và bất kỳ tính năng nào cần tồn tại lâu dài đều phải được xây dựng để thích ứng với điều đó thay vì chống lại nó. Chúng tôi mặc định mọi tiện ích mở rộng dùng tập quyền hẹp nhất mà chức năng thực tế của nó cần, và chỉ mở rộng khi một tính năng cụ thể đòi hỏi.
Keystore
Bên dưới build Android là một artifact bạn không bao giờ nhìn thấy và không thể để mất: khóa ký (signing key). Mất nó, bạn không chỉ mất khả năng cập nhật ứng dụng — bạn mất khả năng cập nhật nó dưới danh tính hiện có, vĩnh viễn, không có con đường khôi phục nào mà Google sẽ cấp cho bạn. Đó không phải hạ tầng gì hào nhoáng. Đó chỉ là một file. Nhưng đó là sự khác biệt giữa việc phát hành phiên bản sáu tháng sau như một bản cập nhật liền mạch, và phát hành nó như một danh sách hoàn toàn mới bắt đầu từ số 0 lượt cài đặt và 0 đánh giá. Chúng tôi tạo ra một khóa cho mỗi dự án và lưu giữ nó để mọi build trong tương lai đều ký bằng cùng một khóa như ngày đầu tiên.
Luồng chat bên dưới tất cả những điều này
Không có điều nào ở trên tồn tại trong một "dự án di động" riêng biệt. Đó vẫn là cùng một cuộc trò chuyện đã xây dựng ứng dụng web. Yêu cầu thay đổi giao diện, bản build web sẽ cập nhật; yêu cầu một gói Android tiếp theo, và nó sẽ được biên dịch từ chính trạng thái hiện tại đó, chứ không phải từ một nhánh đã trôi dạt và lệch pha ba tuần trước. Hầu hết các đội mà tôi từng quan sát cố gắng gắn thêm phần native sau này thường kết cục phải duy trì hai codebase ngày càng xa nhau — một ứng dụng web phát hành mỗi ngày và một lớp bọc native mà ai đó phải nhớ để cập nhật theo kịp trước mỗi lần phát hành. Khoảng cách đó chính là nơi sự lỗi thời tồn tại, và đó chính xác là điều mà một lịch sử build duy nhất loại bỏ. Tuy nhiên điều này cũng có hai mặt: nếu gần đây cuộc trò chuyện diễn ra vội vàng và lỏng lẻo, bản build Android cũng sẽ thừa hưởng điều đó. Đây không phải là một bước hoàn thiện riêng biệt, mà là bản biên dịch trực tiếp của bất cứ thứ gì đang thực sự tồn tại — điều này trên thực tế buộc mọi người phải trung thực, vì không có "chúng ta sẽ dọn dẹp nó trước khi nộp" như một nhiệm vụ phụ để bỏ qua.
Khi đã sẵn sàng để lên cửa hàng ứng dụng, lộ trình phát hành sẽ bàn giao cho danh sách Play Store của chính bạn và tài khoản Apple Developer của chính bạn. Không phải của chúng tôi. Chúng tôi không muốn đứng chen giữa bạn và kênh phân phối của riêng bạn.



