Quản lý phần phụ thuộc

Báo cáo vấn đề Xem nguồn Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Khi xem qua các trang trước, bạn sẽ thấy một chủ đề lặp đi lặp lại: việc quản lý mã của riêng bạn khá đơn giản, nhưng việc quản lý các phần phụ thuộc của mã lại khó khăn hơn nhiều. Có nhiều loại phần phụ thuộc: đôi khi có phần phụ thuộc trên một tác vụ (chẳng hạn như "đẩy tài liệu trước khi tôi đánh dấu bản phát hành là hoàn tất") và đôi khi có phần phụ thuộc trên một cấu phần phần mềm (chẳng hạn như "Tôi cần có phiên bản mới nhất của thư viện thị giác máy tính để tạo mã của mình"). Đôi khi, bạn có các phần phụ thuộc nội bộ trên một phần khác của cơ sở mã và đôi khi bạn có các phần phụ thuộc bên ngoài trên mã hoặc dữ liệu thuộc sở hữu của một nhóm khác (trong tổ chức của bạn hoặc bên thứ ba). Nhưng trong mọi trường hợp, ý tưởng “Tôi cần điều đó trước khi có thể có điều này” là ý tưởng lặp đi lặp lại nhiều lần trong thiết kế của hệ thống xây dựng và quản lý các phần phụ thuộc có lẽ là công việc cơ bản nhất của hệ thống xây dựng.

Xử lý mô-đun và phần phụ thuộc

Các dự án sử dụng hệ thống xây dựng dựa trên cấu phần phần mềm như Bazel được chia thành một nhóm mô-đun, trong đó các mô-đun thể hiện phần phụ thuộc lẫn nhau thông qua tệp BUILD. Việc sắp xếp các mô-đun và phần phụ thuộc này một cách hợp lý có thể ảnh hưởng rất lớn đến cả hiệu suất của hệ thống xây dựng và lượng công việc cần thiết để duy trì hệ thống đó.

Sử dụng mô-đun chi tiết và quy tắc 1:1:1

Câu hỏi đầu tiên xuất hiện khi xây dựng cấu trúc cho một bản dựng dựa trên cấu phần phần mềm là quyết định số lượng chức năng mà một mô-đun riêng lẻ sẽ bao gồm. Trong Bazel, một mô-đun được biểu thị bằng một mục tiêu chỉ định một đơn vị có thể tạo bản dựng như java_library hoặc go_binary. Ở một mức độ nào đó, toàn bộ dự án có thể nằm trong một mô-đun duy nhất bằng cách đặt một tệp BUILD ở thư mục gốc và kết hợp đệ quy tất cả các tệp nguồn của dự án đó. Ở một cực khác, gần như mọi tệp nguồn đều có thể được tạo thành mô-đun riêng, yêu cầu hiệu quả mỗi tệp phải liệt kê trong tệp BUILD mọi tệp khác mà tệp đó phụ thuộc vào.

Hầu hết các dự án đều nằm ở đâu đó giữa hai thái cực này và lựa chọn này liên quan đến việc đánh đổi giữa hiệu suất và khả năng bảo trì. Việc sử dụng một mô-đun duy nhất cho toàn bộ dự án có thể có nghĩa là bạn không bao giờ cần chạm vào tệp BUILD, ngoại trừ khi thêm một phần phụ thuộc bên ngoài, nhưng điều đó có nghĩa là hệ thống xây dựng phải luôn tạo toàn bộ dự án cùng một lúc. Tức là ứng dụng sẽ không thể song song hoặc phân phối các phần của bản dựng cũng như không thể lưu các phần đã được tạo vào bộ nhớ đệm. Mô-đun-cho-mỗi-tệp thì ngược lại: hệ thống xây dựng có độ linh hoạt tối đa trong việc lưu vào bộ nhớ đệm và lên lịch các bước của bản dựng, nhưng các kỹ sư cần phải nỗ lực nhiều hơn để duy trì danh sách các phần phụ thuộc mỗi khi thay đổi tệp tham chiếu nào.

Mặc dù độ chi tiết chính xác thay đổi theo ngôn ngữ (và thường ngay cả trong ngôn ngữ), nhưng Google có xu hướng ưu tiên các mô-đun nhỏ hơn đáng kể so với mô-đun mà bạn thường viết trong hệ thống xây dựng dựa trên tác vụ. Một tệp nhị phân phát hành thông thường tại Google thường phụ thuộc vào hàng chục nghìn mục tiêu và ngay cả một nhóm có quy mô trung bình cũng có thể sở hữu vài trăm mục tiêu trong cơ sở mã của mình. Đối với các ngôn ngữ như Java có khái niệm tích hợp mạnh về đóng gói, mỗi thư mục thường chứa một gói, mục tiêu và tệp BUILD (Pants, một hệ thống xây dựng khác dựa trên Bazel, gọi đây là quy tắc 1:1:1). Các ngôn ngữ có quy ước đóng gói yếu hơn thường xác định nhiều mục tiêu cho mỗi tệp BUILD.

Lợi ích của các mục tiêu bản dựng nhỏ hơn thực sự bắt đầu thể hiện trên quy mô lớn vì các mục tiêu này dẫn đến các bản dựng được phân phối nhanh hơn và ít cần phải tạo lại mục tiêu hơn. Những lợi thế này trở nên hấp dẫn hơn nữa sau khi quá trình kiểm thử bắt đầu, vì các mục tiêu chi tiết hơn có nghĩa là hệ thống xây dựng có thể thông minh hơn nhiều khi chỉ chạy một tập hợp con kiểm thử hạn chế có thể chịu ảnh hưởng của bất kỳ thay đổi nào. Vì Google tin tưởng vào lợi ích hệ thống của việc sử dụng các mục tiêu nhỏ hơn, nên chúng tôi đã có một số bước tiến trong việc giảm thiểu nhược điểm bằng cách đầu tư vào công cụ để tự động quản lý các tệp BUILD nhằm tránh gây gánh nặng cho nhà phát triển.

Một số công cụ trong số này, chẳng hạn như buildifierbuildozer, có sẵn với Bazel trong thư mục buildtools.

Giảm thiểu chế độ hiển thị mô-đun

Bazel và các hệ thống xây dựng khác cho phép mỗi mục tiêu chỉ định một chế độ hiển thị – đây là thuộc tính xác định những mục tiêu nào khác có thể phụ thuộc vào mục tiêu đó. Bạn chỉ có thể tham chiếu đến mục tiêu riêng tư trong tệp BUILD của mục tiêu đó. Một mục tiêu có thể cấp quyền hiển thị rộng hơn cho các mục tiêu của danh sách tệp BUILD được xác định rõ ràng hoặc trong trường hợp chế độ hiển thị công khai, cho mọi mục tiêu trong không gian làm việc.

Giống như hầu hết các ngôn ngữ lập trình, tốt nhất bạn nên giảm thiểu khả năng hiển thị càng nhiều càng tốt. Nhìn chung, các nhóm tại Google sẽ chỉ công khai các mục tiêu nếu các mục tiêu đó đại diện cho các thư viện được sử dụng rộng rãi mà mọi nhóm tại Google đều có thể sử dụng. Những nhóm yêu cầu người khác phối hợp với họ trước khi sử dụng mã sẽ duy trì danh sách cho phép chứa các mục tiêu khách hàng dưới dạng chế độ hiển thị của mục tiêu. Các mục tiêu triển khai nội bộ của mỗi nhóm sẽ chỉ bị giới hạn ở các thư mục do nhóm sở hữu và hầu hết các tệp BUILD sẽ chỉ có một mục tiêu không phải là mục tiêu riêng tư.

Quản lý phần phụ thuộc

Các mô-đun cần có khả năng tham chiếu lẫn nhau. Nhược điểm của việc chia cơ sở mã thành các mô-đun chi tiết là bạn cần quản lý các phần phụ thuộc giữa các mô-đun đó (mặc dù các công cụ có thể giúp tự động hoá việc này). Việc thể hiện các phần phụ thuộc này thường là phần lớn nội dung trong tệp BUILD.

Phần phụ thuộc nội bộ

Trong một dự án lớn được chia thành các mô-đun chi tiết, hầu hết các phần phụ thuộc đều có thể nằm trong nội bộ; tức là trên một mục tiêu khác được xác định và tạo trong cùng một kho lưu trữ nguồn. Các phần phụ thuộc nội bộ khác với các phần phụ thuộc bên ngoài vì chúng được tạo từ nguồn thay vì được tải xuống dưới dạng cấu phần phần mềm tạo sẵn trong khi chạy bản dựng. Điều này cũng có nghĩa là không có khái niệm "phiên bản" cho các phần phụ thuộc nội bộ – một mục tiêu và tất cả các phần phụ thuộc nội bộ của mục tiêu đó luôn được tạo tại cùng một thay đổi/bản sửa đổi trong kho lưu trữ. Một vấn đề cần được xử lý cẩn thận liên quan đến các phần phụ thuộc nội bộ là cách xử lý các phần phụ thuộc bắc cầu (Hình 1). Giả sử mục tiêu A phụ thuộc vào mục tiêu B, nhưng mục tiêu này phụ thuộc vào một mục tiêu thư viện chung C. Mục tiêu A có thể sử dụng các lớp được xác định trong mục tiêu C không?

Phần phụ thuộc bắc cầu

Hình 1 Phần phụ thuộc bắc cầu

Đối với các công cụ cơ bản, điều này không có vấn đề gì; cả B và C sẽ được liên kết vào mục tiêu A khi được tạo, vì vậy, mọi biểu tượng được xác định trong C đều được A biết. Bazel đã cho phép điều này trong nhiều năm, nhưng khi Google phát triển, chúng tôi bắt đầu thấy vấn đề. Giả sử B được tái cấu trúc để không cần phụ thuộc vào C nữa. Nếu phần phụ thuộc của B trên C bị xoá, thì A và mọi mục tiêu khác sử dụng C thông qua phần phụ thuộc trên B sẽ bị lỗi. Về cơ bản, các phần phụ thuộc của một mục tiêu đã trở thành một phần của hợp đồng công khai và không bao giờ có thể được thay đổi một cách an toàn. Điều này có nghĩa là các phần phụ thuộc tích luỹ theo thời gian và các bản dựng tại Google bắt đầu chậm lại.

Cuối cùng, Google đã giải quyết vấn đề này bằng cách giới thiệu "chế độ phần phụ thuộc bắc cầu nghiêm ngặt" trong Bazel. Ở chế độ này, Bazel sẽ phát hiện xem một mục tiêu có cố gắng tham chiếu đến một biểu tượng mà không trực tiếp phụ thuộc vào biểu tượng đó hay không. Nếu có, thì sẽ không thành công và sẽ có một lỗi và một lệnh shell có thể được dùng để tự động chèn phần phụ thuộc. Việc triển khai thay đổi này trên toàn bộ cơ sở mã của Google và tái cấu trúc từng một trong số hàng triệu mục tiêu xây dựng để liệt kê rõ các phần phụ thuộc của chúng là nỗ lực trong nhiều năm, nhưng kết quả rất xứng đáng. Các bản dựng của chúng tôi hiện nhanh hơn nhiều do các mục tiêu có ít phần phụ thuộc không cần thiết hơn và các kỹ sư có thể xoá các phần phụ thuộc không cần thiết mà không lo ngại về việc phá vỡ các mục tiêu phụ thuộc vào các phần phụ thuộc đó.

Như thường lệ, việc thực thi các phần phụ thuộc bắc cầu nghiêm ngặt sẽ có sự đánh đổi. Điều này khiến các tệp bản dựng trở nên dài dòng hơn, vì các thư viện thường dùng hiện cần được liệt kê rõ ràng ở nhiều nơi thay vì được kéo vào ngẫu nhiên, đồng thời các kỹ sư cần phải nỗ lực nhiều hơn để thêm các phần phụ thuộc vào tệp BUILD. Kể từ đó, chúng tôi đã phát triển các công cụ giúp giảm bớt sự phiền toái này bằng cách tự động phát hiện nhiều phần phụ thuộc bị thiếu và thêm các phần phụ thuộc đó vào tệp BUILD mà không cần nhà phát triển can thiệp. Tuy nhiên, ngay cả khi không có các công cụ như vậy, chúng tôi nhận thấy việc đánh đổi này rất đáng giá khi cơ sở mã mở rộng: việc thêm một phần phụ thuộc vào tệp BUILD một cách rõ ràng là một chi phí một lần, nhưng việc xử lý các phần phụ thuộc bắc cầu ngầm ẩn có thể gây ra các vấn đề liên tục miễn là mục tiêu bản dựng tồn tại. Theo mặc định, Bazel thực thi các phần phụ thuộc bắc cầu nghiêm ngặt trên mã Java.

Phần phụ thuộc bên ngoài

Nếu không phải là phần phụ thuộc nội bộ, thì phần phụ thuộc đó phải là phần phụ thuộc bên ngoài. Phần phụ thuộc bên ngoài là những phần phụ thuộc trên các cấu phần phần mềm được tạo và lưu trữ bên ngoài hệ thống xây dựng. Phần phụ thuộc được nhập trực tiếp từ kho lưu trữ cấu phần phần mềm (thường được truy cập qua Internet) và được sử dụng nguyên trạng thay vì được tạo từ nguồn. Một trong những điểm khác biệt lớn nhất giữa phần phụ thuộc bên ngoài và bên trong là phần phụ thuộc bên ngoài có các phiên bản và các phiên bản đó tồn tại độc lập với mã nguồn của dự án.

Quản lý phần phụ thuộc tự động so với thủ công

Hệ thống xây dựng có thể cho phép quản lý các phiên bản phần phụ thuộc bên ngoài theo cách thủ công hoặc tự động. Khi được quản lý theo cách thủ công, tệp bản dựng sẽ liệt kê rõ ràng phiên bản mà nó muốn tải xuống từ kho lưu trữ cấu phần phần mềm, thường sử dụng chuỗi phiên bản ngữ nghĩa như 1.1.4. Khi được quản lý tự động, tệp nguồn sẽ chỉ định một phạm vi phiên bản được chấp nhận và hệ thống xây dựng luôn tải phiên bản mới nhất xuống. Ví dụ: Gradle cho phép khai báo phiên bản phần phụ thuộc là "1.+" để chỉ định rằng mọi phiên bản nhỏ hoặc bản vá của phần phụ thuộc đều được chấp nhận, miễn là phiên bản chính là 1.

Các phần phụ thuộc được quản lý tự động có thể thuận tiện cho các dự án nhỏ, nhưng thường là nguyên nhân gây ra sự cố đối với các dự án có quy mô không nhỏ hoặc đang được nhiều kỹ sư xử lý. Vấn đề với các phần phụ thuộc được quản lý tự động là bạn không có quyền kiểm soát thời điểm cập nhật phiên bản. Không có cách nào để đảm bảo rằng các bên bên ngoài sẽ không thực hiện các bản cập nhật gây lỗi (ngay cả khi họ tuyên bố sử dụng tính năng tạo phiên bản ngữ nghĩa). Vì vậy, một bản dựng hoạt động vào ngày hôm nay có thể bị lỗi vào ngày hôm sau mà không có cách nào dễ dàng để phát hiện những gì đã thay đổi hoặc để khôi phục bản dựng đó về trạng thái hoạt động. Ngay cả khi bản dựng không bị lỗi, có thể có những thay đổi về hiệu suất hoặc hành vi tinh vi mà bạn không thể theo dõi.

Ngược lại, vì các phần phụ thuộc được quản lý theo cách thủ công yêu cầu thay đổi trong chế độ kiểm soát nguồn, nên bạn có thể dễ dàng phát hiện và khôi phục các phần phụ thuộc đó, đồng thời có thể kiểm tra phiên bản cũ của kho lưu trữ để tạo bằng các phần phụ thuộc cũ. Bazel yêu cầu bạn phải chỉ định phiên bản của tất cả các phần phụ thuộc theo cách thủ công. Ngay cả ở quy mô trung bình, chi phí quản lý phiên bản theo cách thủ công cũng rất đáng để mang lại độ ổn định.

Quy tắc một phiên bản

Các phiên bản khác nhau của một thư viện thường được biểu thị bằng các cấu phần phần mềm khác nhau, vì vậy, về lý thuyết, không có lý do gì khiến các phiên bản khác nhau của cùng một phần phụ thuộc bên ngoài không thể được khai báo trong hệ thống xây dựng dưới các tên khác nhau. Bằng cách đó, mỗi mục tiêu có thể chọn phiên bản phần phụ thuộc mà mục tiêu đó muốn sử dụng. Điều này gây ra rất nhiều vấn đề trong thực tế, vì vậy, Google thực thi Quy tắc một phiên bản nghiêm ngặt đối với tất cả các phần phụ thuộc của bên thứ ba trong cơ sở mã của chúng tôi.

Vấn đề lớn nhất khi cho phép nhiều phiên bản là vấn đề về phần phụ thuộc kim cương. Giả sử mục tiêu A phụ thuộc vào mục tiêu B và phiên bản 1 của thư viện bên ngoài. Nếu sau này mục tiêu B được tái cấu trúc để thêm phần phụ thuộc vào v2 của cùng một thư viện bên ngoài, thì mục tiêu A sẽ bị lỗi vì mục tiêu này hiện phụ thuộc ngầm vào hai phiên bản khác nhau của cùng một thư viện. Vì vậy, bạn không nên thêm phần phụ thuộc mới từ một mục tiêu vào bất kỳ thư viện bên thứ ba nào có nhiều phiên bản, vì bất kỳ người dùng nào trong số đó cũng có thể đang phụ thuộc vào một phiên bản khác. Việc tuân thủ Quy tắc một phiên bản sẽ giúp tránh được xung đột này – nếu một mục tiêu thêm một phần phụ thuộc vào thư viện bên thứ ba, thì mọi phần phụ thuộc hiện có sẽ nằm trên cùng một phiên bản đó, vì vậy, các phần phụ thuộc này có thể cùng tồn tại một cách vui vẻ.

Phần phụ thuộc bên ngoài bắc cầu

Việc xử lý các phần phụ thuộc bắc cầu của một phần phụ thuộc bên ngoài có thể đặc biệt khó khăn. Nhiều kho lưu trữ cấu phần phần mềm, chẳng hạn như Maven Central, cho phép các cấu phần phần mềm chỉ định các phần phụ thuộc trên các phiên bản cụ thể của các cấu phần phần mềm khác trong kho lưu trữ. Theo mặc định, các công cụ xây dựng như Maven hoặc Gradle thường tải xuống đệ quy từng phần phụ thuộc bắc cầu, nghĩa là việc thêm một phần phụ thuộc duy nhất vào dự án có thể khiến tổng cộng hàng chục cấu phần phần mềm được tải xuống.

Điều này rất thuận tiện: khi thêm một phần phụ thuộc vào một thư viện mới, bạn sẽ phải theo dõi từng phần phụ thuộc bắc cầu của thư viện đó và thêm tất cả các phần phụ thuộc đó theo cách thủ công. Tuy nhiên, cũng có một nhược điểm lớn: vì các thư viện khác nhau có thể phụ thuộc vào các phiên bản khác nhau của cùng một thư viện bên thứ ba, nên chiến lược này nhất thiết phải vi phạm Quy tắc một phiên bản và dẫn đến vấn đề phần phụ thuộc kim cương. Nếu mục tiêu của bạn phụ thuộc vào hai thư viện bên ngoài sử dụng các phiên bản khác nhau của cùng một phần phụ thuộc, thì không thể biết bạn sẽ nhận được phần phụ thuộc nào. Điều này cũng có nghĩa là việc cập nhật một phần phụ thuộc bên ngoài có thể gây ra các lỗi dường như không liên quan trong toàn bộ cơ sở mã nếu phiên bản mới bắt đầu kéo các phiên bản xung đột của một số phần phụ thuộc.

Vì lý do này, Bazel không tự động tải các phần phụ thuộc bắc cầu xuống. Và rất tiếc, không có giải pháp nào là giải pháp dễ dàng hơn. Giải pháp thay thế cho Bazel là yêu cầu một tệp chung liệt kê mọi phần phụ thuộc bên ngoài của kho lưu trữ và một phiên bản rõ ràng dùng cho phần phụ thuộc đó trong toàn bộ kho lưu trữ. May mắn thay, Bazel cung cấp các công cụ có thể tự động tạo một tệp như vậy chứa các phần phụ thuộc bắc cầu của một tập hợp cấu phần phần mềm Maven. Bạn có thể chạy công cụ này một lần để tạo tệp WORKSPACE ban đầu cho dự án, sau đó tệp đó có thể được cập nhật theo cách thủ công để điều chỉnh phiên bản của từng phần phụ thuộc.

Một lần nữa, bạn phải chọn giữa sự tiện lợi và khả năng mở rộng. Các dự án nhỏ có thể không muốn phải lo lắng về việc tự quản lý các phần phụ thuộc bắc cầu và có thể sử dụng các phần phụ thuộc bắc cầu tự động. Chiến lược này ngày càng trở nên ít hấp dẫn hơn khi tổ chức và cơ sở mã phát triển, đồng thời các xung đột và kết quả không mong muốn ngày càng trở nên thường xuyên hơn. Ở quy mô lớn hơn, chi phí quản lý các phần phụ thuộc theo cách thủ công sẽ thấp hơn nhiều so với chi phí xử lý các vấn đề do tính năng tự động quản lý phần phụ thuộc gây ra.

Lưu kết quả bản dựng vào bộ nhớ đệm bằng các phần phụ thuộc bên ngoài

Các phần phụ thuộc bên ngoài thường do các bên thứ ba cung cấp, những bên này phát hành các phiên bản thư viện ổn định, có thể không cung cấp mã nguồn. Một số tổ chức cũng có thể chọn cung cấp một số mã của riêng họ dưới dạng cấu phần phần mềm, cho phép các đoạn mã khác phụ thuộc vào chúng dưới dạng bên thứ ba thay vì phần phụ thuộc nội bộ. Về mặt lý thuyết, tính năng này có thể tăng tốc các bản dựng nếu cấu phần phần mềm được tạo chậm nhưng tải xuống nhanh.

Tuy nhiên, điều này cũng gây ra nhiều hao tổn và phức tạp: cần có người chịu trách nhiệm tạo từng cấu phần phần mềm đó và tải lên kho lưu trữ cấu phần phần mềm, đồng thời khách hàng cần đảm bảo rằng họ luôn cập nhật phiên bản mới nhất. Việc gỡ lỗi cũng trở nên khó khăn hơn nhiều vì các phần khác nhau của hệ thống sẽ được tạo từ các điểm khác nhau trong kho lưu trữ và không còn chế độ xem nhất quán của cây nguồn.

Một cách tốt hơn để giải quyết vấn đề các cấu phần phần mềm mất nhiều thời gian để tạo là sử dụng hệ thống xây dựng hỗ trợ lưu vào bộ nhớ đệm từ xa, như mô tả ở trên. Một hệ thống xây dựng như vậy sẽ lưu các cấu phần phần mềm kết quả từ mọi bản dựng đến một vị trí được chia sẻ giữa các kỹ sư. Vì vậy, nếu nhà phát triển phụ thuộc vào cấu phần phần mềm mới được người khác xây dựng gần đây, thì hệ thống xây dựng sẽ tự động tải cấu phần phần mềm đó xuống thay vì tạo. Điều này mang lại tất cả các lợi ích về hiệu suất khi trực tiếp phụ thuộc vào cấu phần phần mềm, đồng thời vẫn đảm bảo rằng các bản dựng nhất quán như thể chúng luôn được tạo từ cùng một nguồn. Đây là chiến lược được Google sử dụng nội bộ và Bazel có thể được định cấu hình để sử dụng bộ nhớ đệm từ xa.

Tính bảo mật và độ tin cậy của các phần phụ thuộc bên ngoài

Việc phụ thuộc vào cấu phần phần mềm từ các nguồn của bên thứ ba vốn tiềm ẩn rủi ro. Có rủi ro về khả năng sử dụng nếu nguồn của bên thứ ba (chẳng hạn như kho lưu trữ cấu phần phần mềm) ngừng hoạt động, vì toàn bộ bản dựng của bạn có thể bị tạm dừng nếu không thể tải một phần phụ thuộc bên ngoài xuống. Ngoài ra còn có một rủi ro bảo mật: nếu hệ thống của bên thứ ba bị kẻ tấn công xâm phạm, kẻ tấn công có thể thay thế cấu phần phần mềm được tham chiếu bằng một trong các thiết kế của riêng chúng, cho phép chúng chèn mã tuỳ ý vào bản dựng của bạn. Bạn có thể giảm thiểu cả hai vấn đề này bằng cách phản chiếu mọi cấu phần phần mềm mà bạn phụ thuộc vào các máy chủ mà bạn kiểm soát và chặn hệ thống xây dựng truy cập vào các kho lưu trữ cấu phần phần mềm của bên thứ ba như Maven Central. Tuy nhiên, bạn cần phải tốn công sức và tài nguyên để duy trì các bản sao này. Vì vậy, việc có nên sử dụng các bản sao đó hay không thường phụ thuộc vào quy mô của dự án. Bạn cũng có thể ngăn chặn hoàn toàn vấn đề bảo mật với chi phí thấp bằng cách yêu cầu chỉ định hàm băm của từng cấu phần phần mềm bên thứ ba trong kho lưu trữ nguồn, khiến bản dựng không thành công nếu cấu phần phần mềm bị can thiệp. Một giải pháp thay thế khác giúp bạn hoàn toàn tránh được vấn đề này là chuyển các phần phụ thuộc của dự án cho nhà cung cấp. Khi một dự án bán các phần phụ thuộc, dự án đó sẽ kiểm tra các phần phụ thuộc đó vào hệ thống quản lý nguồn cùng với mã nguồn của dự án, dưới dạng nguồn hoặc dưới dạng tệp nhị phân. Điều này có nghĩa là tất cả các phần phụ thuộc bên ngoài của dự án đều được chuyển đổi thành các phần phụ thuộc nội bộ. Google sử dụng phương pháp này trong nội bộ, kiểm tra mọi thư viện bên thứ ba được tham chiếu trên toàn bộ Google vào thư mục third_party ở gốc của cây nguồn của Google. Tuy nhiên, cách này chỉ hoạt động tại Google vì hệ thống kiểm soát nguồn của Google được tạo tuỳ chỉnh để xử lý một monorepo cực lớn, vì vậy, việc mua lại có thể không phải là lựa chọn cho tất cả các tổ chức.