7 điểm bởi GN⁺ 2025-05-10 | 3 bình luận | Chia sẻ qua WhatsApp
  • Hệ thống quản lý dependency của Rust giúp việc phát triển thuận tiện hơn, nhưng số lượng và chất lượng dependency lại là điều đáng băn khoăn
  • Ngay cả những crate được dùng phổ biến cũng có thể không còn được cập nhật, nên đôi khi tự triển khai lại còn tốt hơn
  • Sau khi thêm các crate nổi tiếng như Axum, Tokio, tổng số dòng mã tính cả dependency lên tới 3,6 triệu, rất khó kiểm soát
  • Phần mã tôi thực sự viết chỉ khoảng 1.000 dòng, nhưng gần như không thể rà soát và kiểm toán phần mã xung quanh
  • Vẫn chưa có giải pháp rõ ràng về việc có nên mở rộng thư viện chuẩn của Rust hay cách triển khai hạ tầng cốt lõi; toàn bộ cộng đồng đang phải cùng cân nhắc sự cân bằng giữa hiệu năng, an toàn và khả năng bảo trì

Tổng quan về vấn đề dependency trong Rust

  • Rust là ngôn ngữ tôi yêu thích nhất, với cộng đồng và trải nghiệm sử dụng ngôn ngữ rất xuất sắc
  • Năng suất phát triển cao, nhưng gần đây tôi bắt đầu lo lắng ở khía cạnh quản lý dependency

Ưu điểm của crate Rust và Cargo

  • Với Cargo, có thể quản lý package và tự động hóa quy trình build, giúp tăng năng suất đáng kể
  • Việc chuyển đổi giữa nhiều hệ điều hành và kiến trúc trở nên dễ dàng hơn, không cần quá bận tâm đến việc tự quản lý file hay cấu hình công cụ build
  • Có thể bắt tay vào viết code ngay mà không phải suy nghĩ nhiều về quản lý package riêng

Nhược điểm của việc quản lý crate Rust

  • Vì ít phải bận tâm đến quản lý package hơn nên dễ lơ là về độ ổn định
  • Ví dụ, tôi từng dùng crate dotenv, rồi biết qua Security Advisory rằng nó đã ngừng được bảo trì
  • Khi cân nhắc crate thay thế (dotenvy), tôi nhận ra phần mình thực sự cần chỉ khoảng 35 dòng nên đã tự triển khai
  • Việc package bị bỏ bảo trì xảy ra thường xuyên ở nhiều ngôn ngữ, nên cốt lõi vấn đề là tình huống phụ thuộc là điều khó tránh khỏi

Dependency khiến lượng mã tăng vọt

  • Tôi đang dùng những package quan trọng và chất lượng cao của hệ sinh thái Rust như Tokio, Axum
  • Tôi đã thêm Axum, Reqwest, ripunzip, serde, serde_json, tokio, tower-http, tracing, tracing-subscriber dưới dạng dependency
  • Mục đích chính chỉ là web server, giải nén file và logging nên bản thân project khá đơn giản
  • Tôi dùng tính năng Cargo vendor để tải toàn bộ crate phụ thuộc về máy cục bộ
  • Khi phân tích số dòng mã bằng tokei, tổng cộng khoảng 3,6 triệu dòng nếu tính cả dependency (nếu bỏ các crate đã vendor thì khoảng 11.136 dòng)
  • Để tham chiếu, toàn bộ nhân Linux được cho là khoảng 27,8 triệu dòng, nghĩa là project nhỏ của tôi đã bằng khoảng một phần bảy con số đó
  • Phần mã tôi thực sự viết chỉ khoảng 1.000 dòng
  • Việc theo dõi và kiểm toán khối lượng mã phụ thuộc lớn như vậy gần như là bất khả thi

Trăn trở về lời giải

  • Hiện tại vẫn chưa có giải pháp rõ ràng
  • Một số người cho rằng nên mở rộng thư viện chuẩn như Go, nhưng điều đó cũng tạo ra vấn đề mới như gánh nặng bảo trì
  • Rust theo đuổi hiệu năng cao, an toàn và tính mô-đun, đồng thời nhắm tới cạnh tranh trong embedded hoặc với C++, nên việc mở rộng thư viện chuẩn cần hết sức thận trọng
  • Ví dụ, những runtime cao cấp như Tokio cũng đang được quản lý rất tích cực trên GitHub và Discord
  • Trên thực tế, việc tự triển khai hạ tầng cốt lõi như async runtime hay web server là quá sức với một lập trình viên cá nhân
  • Ngay cả Cloudflare, một dịch vụ quy mô lớn, cũng vẫn dùng nguyên tokio và dependency từ crates.io; còn họ kiểm toán chúng thường xuyên đến đâu thì không rõ
  • Clickhouse cũng từng nhắc đến vấn đề kích thước binary và số lượng crate
  • Với Cargo, rất khó xác định chính xác những dòng mã nào thực sự được đưa vào binary cuối cùng, và vẫn có hạn chế là mã không cần thiết theo từng nền tảng cũng bị kéo vào
  • Cuối cùng, thực tế là chỉ còn cách đặt câu hỏi này cho cả cộng đồng cùng tìm lời giải

3 bình luận

 
codemasterkimc 2025-05-11

Nếu chạy Trivy thì số lỗ hổng mức high hoặc critical ít hơn rất nhiều so với js NPM hay Java Maven nên an toàn hơn, vậy bài viết này muốn dùng Rust để lập luận điều gì?

 
GN⁺ 2025-05-10
Ý kiến trên Hacker News
  • Theo tôi, bất kỳ hệ thống nào cho phép thêm dependency một cách "dễ dàng" mà không bị phạt về kích thước hay chi phí thì sớm muộn cũng sẽ dẫn đến vấn đề dependency. Nhìn lại cách phần mềm được phân phối trong 40 năm qua: vào thập niên 80, thư viện phải mua bằng tiền, và trong môi trường bị giới hạn dung lượng thì người ta chỉ chọn đúng phần cần thiết để đưa vào. Ngày nay thì người ta cứ chồng thêm thư viện lên trên thư viện. Chỉ cần một dòng import foolib, và chẳng ai bận tâm bên trong có gì. Ở mỗi tầng có lẽ chỉ cần khoảng 5% chức năng, nhưng cây phụ thuộc càng sâu thì mã vô dụng càng tích tụ. Kết quả là một binary đơn giản cũng có thể thành 500MiB, chỉ vì cần mỗi việc định dạng số mà phải kéo cả dependency vào. Go hay Rust còn khuyến khích dồn mọi thứ vào một file, nên nếu chỉ muốn dùng một phần thì rất khó xử. Về lâu dài, giải pháp thực sự có lẽ là theo dõi symbol/dependency ở mức siêu chi tiết, để mọi hàm/kiểu đều khai báo chính xác phần tử mình cần, chỉ lấy đúng lượng mã cần thiết và bỏ phần còn lại. Cá nhân tôi không thích ý tưởng này lắm, nhưng tôi cũng không nghĩ ra cách nào khác để giải quyết hệ thống hiện tại, nơi cả vũ trụ bị kéo theo từ cây dependency
    • Có thể vì bạn vẫn còn là sinh viên nên chưa rõ, nhưng trình biên dịch Rust đã phát hiện được mã, biến, hàm... không dùng đến. IDE trong đa số ngôn ngữ cũng làm được việc này. Vậy thì chỉ cần loại bỏ chúng là xong phải không? Mã không dùng đến sẽ không được biên dịch
    • Thực tế, khi tôi làm với một thư viện Rust có cây dependency khá nặng (Xilem) và thử cắt gọn bằng feature flag, thì gần như mọi dependency đều thực sự cần để phục vụ tính năng tương ứng (vulkan, giải mã PNG, unicode shaping, v.v.). Những dependency không cần thường chỉ là các gói rất nhỏ, và tôi chỉ bỏ được serde_json bằng vài chỉnh sửa nhỏ. Các dependency lớn hơn (winit/wgpu...) thì cần thay đổi kiến trúc nên không thể bỏ dễ dàng
    • Go hay C#(.NET) là phản ví dụ tốt. Chúng có quản lý package và hệ sinh thái hiệu quả không kém Rust hay JS(Node), nhưng tương đối ít gặp dependency hell hơn. Lý do là standard library quá tốt. Mà độ đồ sộ của standard library là thứ chỉ các tập đoàn lớn (Google, Microsoft) mới đủ sức đầu tư
    • Vậy thì tại sao compiler hiện tại lại không loại bỏ mã không dùng đến?
    • Trước đây, mỗi hàm thường được biên dịch thành một file .o rồi gói vào archive .a, và linker chỉ lấy ra những hàm cần thiết. Namespace cũng được làm kiểu foolib_do_thing(). Còn hiện tại, theo kiểu god object, mọi hàm đều nằm trong object top-level, nên chỉ cần import foolib là kéo theo cả khối. Trong trạng thái đó, linker rất khó xác định chính xác hàm nào là bắt buộc. Ngược lại, Go làm dead code elimination rất tốt, nên cái gì không dùng sẽ bị cắt khỏi kết quả biên dịch
    • Compiler và linker hiện đại vốn đã làm symbol extraction và dead code elimination rồi, và Rust cũng hỗ trợ điều này qua các dự án như min-sized-rust
    • Trước đây người ta còn quản lý bằng cách đưa toàn bộ thư viện vào dự án rồi tự tích hợp vào file build. Việc này rất tốn công và phiền phức, nhưng khiến bạn hiểu sâu hơn nhiều so với việc chỉ thêm một dòng vào file deps
    • Go thực ra không hề cố chấp với một file duy nhất, mà còn hỗ trợ chia file theo logic rất dễ dàng. Tôi thực sự thích điểm này
    • Dotnet đã hiện thực hóa ý tưởng này thông qua Trimming và Ahead Of Time Compilation. Các ngôn ngữ khác có thể học từ Dotnet
    • Nếu xét ở góc độ kích thước binary, thì LTO(Link Time Optimization) gần như giải quyết hoàn toàn vấn đề này. Phần không dùng đến sẽ bị loại bỏ trong quá trình tối ưu hóa. Chỉ là thời gian build vẫn còn tốn
    • Tôi lại nghĩ bản thân thư viện không phải vấn đề, mà vấn đề là sau khi thêm dependency thì ta thiếu tầm nhìn về bên trong nó đang được dùng thế nào và ở mức độ nào. Cần một môi trường có thể dễ dàng phản hồi về hiệu năng/tỷ lệ mã build dư thừa của từng package
    • Ngôn ngữ Unison áp dụng một cách tiếp cận phần nào tương tự ý tưởng này. Mỗi hàm được định nghĩa theo cấu trúc AST, rồi được tải và tái sử dụng từ một registry toàn cục dựa trên hash
    • So với kiểu bảo trì phân tán các mảnh thư viện nhỏ như isEven, isOdd, leftpad trong npm, thì một thư viện đa dụng lớn do một nhóm liên hiệp quản lý sẽ đảm bảo tương lai và tính liên tục tốt hơn nhiều
    • Thay vì theo đuổi symbol/dependency siêu mịn, thì cấu trúc module siêu nhỏ kết hợp tận dụng các hệ thống tree-shaking sẵn có cũng là một ý tưởng
    • Cách quản lý dependency thực tế của Go khá gần với lý tưởng được mô tả trong bài gốc. Module là tập hợp các package, và khi vendor thì chỉ bao gồm package và symbol thực sự được dùng (tôi không chắc có chính xác đến cấp độ symbol hay không)
    • Hệ thống module của JS đúng là hỗ trợ kiểu quản lý symbol siêu chi tiết và tree shaking như vậy
    • Ý tưởng dependency siêu chi tiết được đề xuất ban đầu thực ra đã được giải quyết bằng cơ chế section splitting như rust --gc-sections
    • Rust là ngôn ngữ hỗ trợ import tinh vi rất tốt thông qua việc chia API bằng crate feature. Khác với Go
    • Tùy kiến trúc, ví dụ một thick client thiên về cục bộ, thì dù cài đặt ban đầu là 800MB cũng không sao, vì khi sử dụng thực tế chỉ cần giao tiếp qua mạng rất hạn chế. Các dependency lớn lặp lại để phục vụ cộng tác trong UI cũng có thể chấp nhận được
    • Cách tốt nhất để tái sử dụng mã chính là dùng dependency như thế này. Chỉ tối ưu ở nơi thực sự cần thiết
    • Ngay từ thập niên 80, khái niệm thành phần phần mềm tái sử dụng đã được hiện thực qua các ngôn ngữ như Objective-C. Một trong những thành công lớn của Rust là đưa kiểu component hóa phần mềm này vào cả ngôn ngữ lập trình hệ thống một cách rộng rãi
    • Tree shaking có thể phần nào giải quyết vấn đề phình to kích thước/mã (thậm chí ở server thì người ta còn không quan tâm). Vấn đề nghiêm trọng hơn là rủi ro chuỗi cung ứng dependency và bảo mật. Doanh nghiệp càng lớn thì càng có quy trình phê duyệt khi dùng mã nguồn mở. Chỉ tăng granularity thôi không có ý nghĩa bảo mật nếu 1000 tính năng đến từ 1000 tác giả NPM khác nhau
    • Nếu mỗi tầng trừu tượng package chỉ được tận dụng 50%, thì ở mỗi tầng kích thước tổng thể sẽ tăng gấp đôi so với nhu cầu thực tế. Qua 3 tầng thì 88% là mã vô dụng. Ví dụ: máy tính trên Windows 11 đi kèm cả những thư viện vô ích (đến cả công cụ khôi phục tài khoản). Đây là ví dụ cho việc sự dễ dàng khi thêm tính năng dẫn đến độ phức tạp tăng lên
    • Tôi đồng ý việc dependency tích lũy là vấn đề. Ở thời điểm hiện tại, cách phòng thủ tốt nhất là quản lý dependency hệ thống cực kỳ nghiêm ngặt. Vì một hàm 10 dòng mà kéo thư viện ngoài vào thì tôi thà tự copy code vào còn hơn. Một hệ sinh thái thư viện lành mạnh là chuyện hiếm. Tôi thường chặn ngay các kỹ sư junior khi họ định thêm dependency bừa bãi
    • Lâu lắm rồi mới thấy ai đó khẳng định mạnh như vậy mà lại còn không hiểu nổi những điều cơ bản về Rust
    • Nhờ dead code elimination, ở các ngôn ngữ biên dịch như Rust, cây dependency lớn không đồng nghĩa với binary bị phình ra
  • Điều tôi thấy có vấn đề ở hệ sinh thái npm là nhiều lập trình viên mang dependency vào mà không hề suy nghĩ về thiết kế. Ví dụ thư viện glob đáng lẽ chỉ nên là một hàm globbing đơn giản, nhưng tác giả lại gói luôn cả công cụ dòng lệnh và thêm parser cồng kềnh vào dependency. Điều đó dẫn đến các cảnh báo "dependency out-of-date" xuất hiện liên tục. Phạm vi trách nhiệm của thư viện glob cũng gây tranh cãi. Chỉ làm pattern matching trên chuỗi sẽ là thiết kế linh hoạt hơn nhiều (dễ test, dễ trừu tượng hóa filesystem). Dù vậy, cũng có nhiều người muốn một thư viện toàn năng kiểu "Do everything", nhưng càng như vậy thì tác dụng phụ càng lớn. Tôi đoán Rust cũng sẽ không khác quá nhiều
    • Cảm quan thiết kế rất quan trọng, nhưng ngôn ngữ tốt thì không ép hay cản trở sở thích phát triển kiểu này của lập trình viên. Rust, Zig, C đều như vậy. Vấn đề chỉ ít xảy ra hơn về mặt thống kê. Khi một "đám đông" lập trình viên tập hợp lại, mô hình "bazaar" sẽ xuất hiện, nơi ai cũng có thể thoải mái chồng crate lên nhau. Về lâu dài, tôi hy vọng Rust cũng sẽ có một standard library chính thức kiểu "bao gồm sẵn pin" với namespace được tổ chức tốt, ví dụ như stdlib::data_structures::automata::weighted_finite_state_transducer. Vì ngôn ngữ này đã tích hợp quản lý phiên bản và tương thích ngược ở cấp ngôn ngữ, nên tôi vẫn kỳ vọng nó tiếp tục cải thiện
    • Hàm glob của POSIX thực sự duyệt filesystem. Nếu cần matching trên chuỗi thì đã có fnmatch. Lý tưởng nhất là để fnmatch ở một module riêng rồi cho glob phụ thuộc vào nó. Tự triển khai glob thật ra khá khó, vì còn hàng loạt yêu cầu phức tạp như cấu trúc thư mục, brace expansion, v.v., nên cần một tổ hợp hàm được thiết kế cẩn thận
    • Trong Rust, borrow checker từng đóng vai trò như một lớp khiên nào đó trước các lập trình viên có cảm quan thiết kế kém. Không rõ tác động này sẽ còn kéo dài đến bao giờ
    • Một trong những ưu điểm lớn của Rust là cộng đồng lập trình viên nhìn chung khá giỏi, và chất lượng crate cũng tương đối cao
    • Bun cũng có sẵn tính năng glob
  • Không cần phải chỉ đích danh Rust một cách không cần thiết; vấn đề dependency và tấn công chuỗi cung ứng đã là hiện thực rồi. Nếu thiết kế một ngôn ngữ mới, có lẽ cần tích hợp sẵn một capability system để cô lập an toàn toàn bộ cây thư viện. Ví dụ, khi thiết kế thư viện tải ảnh, hãy để nó chỉ nhận stream thay vì file, hoặc khai báo rõ là nó "không có quyền mở file", để việc dùng hàm nguy hiểm bị chặn ngay từ lúc biên dịch. Trong các hệ sinh thái hiện tại thì chuyện này không dễ, nhưng nếu làm đúng thì có thể giảm thiểu đáng kể bề mặt tấn công. Văn hóa tối thiểu hóa dependency cũng khó giải quyết tận gốc, và các ngôn ngữ như Go cũng không miễn nhiễm trước tấn công chuỗi cung ứng
    • Cần tích cực lan rộng văn hóa Sans-IO, tức thiết kế không để dependency tự thực hiện IO trực tiếp. Khi có thư viện mới ra mắt, cũng nên có văn hóa chỉ ra nếu nó tự xử lý IO. Dĩ nhiên chỉ dựa vào soi xét công khai là chưa đủ, nhưng sẽ tốt nếu nguyên tắc Sans-IO được phổ biến hơn
    • Một ví dụ là WUFFS, một ngôn ngữ chuyên biệt. Thực tế nó còn không in nổi Hello world, cũng không có kiểu string. Thay vào đó, nó chỉ chuyên cho việc parse các định dạng file không đáng tin cậy. Cần có nhiều ngôn ngữ chuyên biệt kiểu này hơn. Nó vừa nhanh vừa ít rủi ro, nên cũng giảm được các kiểm tra không cần thiết
    • Java và .NET Framework đã từng có partial trust/capabilities từ hàng chục năm trước, nhưng không được dùng rộng rãi và cuối cùng bị loại bỏ
    • Rust cũng có chút xu hướng tương tự. Với #![deny(unsafe_code)], việc dùng unsafe sẽ bị chặn bằng lỗi biên dịch và được thông báo rõ cho người dùng. Tuy nhiên đây không phải là cơ chế kiểm tra bắt buộc tuyệt đối; nếu cho phép đặc biệt thì vẫn có thể dùng unsafe. Có thể hình dung một capability system giống feature flag, cho phép điều chỉnh xuyên suốt các chức năng của standard library
    • Tôi rất muốn tự làm ra ý tưởng kiểu này và hy vọng một ngày nào đó nó thành hiện thực. Trong Rust, theo dõi capability dựa trên linter phần nào là khả thi. Vấn đề unsoundness của compiler thì vẫn cần được giải quyết
    • Dù rất khó đưa ràng buộc tĩnh hoàn toàn vào ngôn ngữ hay hệ sinh thái hiện tại, chỉ cần kiểm chứng ở runtime thôi cũng đã đem lại phần lớn hiệu quả. Nếu biên dịch mã thư viện từ source, có thể đặt wrapper kiểm tra quyền ở từng system call. Khi vi phạm thì panic, đồng thời cần đầu tư viết và phân phối capability profile cho từng thư viện. Điều tương tự thực ra đã được chứng minh phần nào trong hệ sinh thái TypeScript
    • Haskell phần nào hiện thực hóa cách tiếp cận này bằng IO monad. Những hàm không được phép làm IO sẽ bị ràng buộc ngay ở type signature
    • Theo tôi, để có được một cơ chế như vậy thì có lẽ còn phải thay đổi toàn diện cả cách giao tiếp với OS. Ngay cả việc đọc stream cũng có cái bẫy là bên dưới vẫn có thể dùng system call đọc file
    • Có một dự án tên là Capslock hoạt động gần với hướng này trong Go
    • Nếu hạn chế ngay từ chương trình entry để thư viện không thể import system API, thì chỉ với dependency injection cũng đã có thể truyền capability được rồi. Về mặt thiết kế, điều này vẫn làm được trong các ngôn ngữ hiện tại, nhưng vấn đề thực tế là nó phá vỡ khả năng tương thích với thư viện cũ
    • Tôi tò mò không biết trước đây đã từng có gì tương tự ý tưởng này được triển khai chưa. Trong các ngôn ngữ hiện nay, có vẻ cực kỳ khó áp dụng
    • Một ngôn ngữ thôi thì không đủ, cần cả một hệ sinh thái đa ngôn ngữ
    • Trong hệ sinh thái TypeScript, ví dụ nếu môi trường không có class thao tác file thì compile sẽ thất bại, nhờ đó giới hạn được áp dụng một cách tự nhiên
  • Đây là vấn đề phổ quát của phát triển phần mềm hiện đại. Rào cản gia nhập thấp hơn, khả năng tái sử dụng mã cũ nhiều hơn. Dependency, suy cho cùng, là mã không thể mặc định tin cậy. Nếu không có giải pháp kỹ thuật, sẽ luôn cần ai đó tiếp tục làm công việc review code, bảo trì, và duy trì các hệ thống niềm tin mang tính xã hội/pháp lý. Nếu đưa hết vào Rust stdlib thì đội ngũ trung tâm phải gánh trách nhiệm cho toàn bộ số mã đó, nên gánh nặng quản trị sẽ tăng mạnh
    • Mức độ nghiêm trọng bề ngoài khác nhau tùy ngôn ngữ. Những ngôn ngữ có standard library mạnh sẽ có lợi hơn vì có thể làm được nhiều việc với rất ít dependency ngoài. Các ngôn ngữ như JS/Node, vốn có ít chức năng nền tảng, thì dependency ngoài gần như là mặc định. "Nhẹ" không phải lúc nào cũng tốt
    • Tôi nghĩ Rust cần tích hợp thêm nhiều thứ hơn vào standard library. Go có standard library rất tốt, còn với Rust thì ngay cả các chức năng cơ bản như web, tls, x509, base64... cũng khiến việc chọn và quản lý thư viện trở nên đau đầu
    • Gilad Bracha từng đề xuất một hướng tiếp cận thú vị cho sandbox thư viện bên thứ ba: bỏ import, thay mọi thứ bằng dependency injection. Nếu không inject những thứ như IO subsystem thì mã bên thứ ba sẽ tuyệt đối không thể chạm vào đó. Nếu chỉ muốn cấp quyền đọc, thì chỉ cần bọc phần đọc rồi inject đúng phần ấy. Dù vậy, trong lĩnh vực lập trình hệ thống thì cách này có giới hạn (do unsafe code và những yếu tố tương tự)
    • Cũng có đề xuất kiểu QubesOS: chạy mọi thư viện trong môi trường cô lập, mã của chính mình ở dom0, mỗi thư viện ở một template VM riêng, giao tiếp qua network namespace. Với các ngành nhạy cảm thì cách này khá thực tế
    • Theo quan sát của tôi, không phải chúng ta đang làm những việc khó hơn, mà là đang xử lý cùng một việc theo cách phức tạp hơn. Bản thân mục tiêu không hề khó hơn
    • Thực ra mỗi ngôn ngữ lại một kiểu. Với C/C++, việc thêm dependency đã khó, mà muốn hỗ trợ cross-platform thì còn phiền hơn nhiều, nên kiểu vấn đề này ít phát sinh hơn
    • Cái phức tạp ở đây chính là sự phình to mã không cần thiết. Gần như mọi dự án đều đầy rẫy độ phức tạp không cần thiết và overengineering. Đó là vấn đề của cả ngành
  • blessed.rs gợi ý một danh sách thư viện hữu ích khó có thể đưa vào standard library. Tôi thích hệ thống này vì nhờ đó phần lớn package có thể bị giới hạn và quản lý theo mục đích cụ thể
    • cargo-vet cũng rất đáng khuyên dùng. Nó giúp theo dõi và xác định package đáng tin cậy, ví dụ từ các package phải được chuyên gia audit trước khi import, cho tới các chính sách semi-YOLO như cứ tin những package do maintainer của tokio quản lý. Nó formal hơn blessed.rs một chút, và rất phù hợp để chia sẻ danh sách bán-chuẩn chính thức trong nội bộ đội ngũ
    • Giá mà Python cũng có hệ thống như vậy thì tốt biết mấy
    • Tôi xem thử rồi, đúng là một dự án gợi ý rất ổn
  • Sau vụ leftpad, cái nhìn tiêu cực về package manager vẫn còn tồn tại. Những thứ như tokio thực chất gần như là tính năng cấp ngôn ngữ, nên nếu OP cho rằng phải tự audit cả Go hay thậm chí V8 của Node thì điều đó không thực tế
    • Thực tế thì tokio cũng vẫn luôn có ai đó audit đều đặn. Không phải số đông, nhưng dù sao cũng có người làm
    • Hiện tượng cargo đưa cả hai version vào khi hai dependency dùng hai version khác nhau là một điểm mà cargo hỗ trợ khá đặc biệt
  • Feature flag trong package cargo là một điểm rất hay. Tôi thường gửi PR để giấu các dependency không cần thiết đằng sau những flag này. Có thể xem cây dependency rất dễ bằng cargo tree. Góc nhìn số dòng mã thực sự đi vào binary thì lại không có nhiều ý nghĩa; vì khi hàm bị inline thì phần lớn cuối cùng dồn hết vào main
    • Tiếc là npm không có feature flag. Tôi tự hỏi có package manager nào hỗ trợ chuyện này không. Tôi muốn cô lập phần mã phụ thuộc framework cụ thể trong thư viện nội bộ để mở rộng nó
  • Tôi cũng thấy tương tự. Việc thêm dependency bằng Cargo dễ đến mức dù bản thân có cẩn thận đi nữa, chỉ cần thêm vài cái là hàng chục transitive dependency kéo theo ngay. Nhưng bảo là đừng dùng thì cũng không thực tế. Trong C++ hiện tượng này ít thấy hơn. Rust có xu hướng tách package rất nhỏ, nên tạo cảm giác như đang lôi mã ngẫu nhiên từ Internet về. Tôi thích Rust, nhưng không thích cấu trúc kiểu này
    • Trong bài được link trên subreddit Rust có nói rằng lý do dependency trong C++ trông ít lộ diện hơn là vì đa số được cung cấp dưới dạng dynamic library. Thực ra việc dựa vào khả năng quản lý ổn định/bảo mật của OS package manager cũng là một lợi thế. Sẽ hay hơn nếu Rust có khái niệm mở rộng standard library
    • Vì dependency C++ phức tạp và hệ thống build thì hỗn loạn, nên tôi nghĩ dependency kiểu Rust dù kém ổn định hơn vẫn còn tốt hơn. Dependency bắc cầu thực tế trong C++ thậm chí còn ít lộ diện hơn vì chúng thường ở dạng tiền biên dịch
    • Trong Rust, việc chia package nhỏ không hẳn là một "triết lý" mà chủ yếu do tốc độ build. Khi quy mô tăng lên, người ta chia dự án thành crate. Không phải để trừu tượng hóa, mà là vì hiệu năng build buộc họ phải tái cấu trúc theo hướng đó
    • Không nhất thiết phải đồng ý vô điều kiện với lập luận kiểu "thế thì đừng dùng". Chuyện này đáng để suy nghĩ thêm
    • C++ và CMake khó đến mức trong thực tế nhiều phần mềm rốt cuộc bị bỏ luôn không dùng
  • Tôi quản lý theo kiểu: thư viện cốt lõi thì dùng mã nguồn mở, còn các chức năng nhỏ thì tham khảo từ mã nguồn mở rồi tự copy-paste vào code của mình. Mã sẽ to lên đôi chút một cách không cần thiết, nhưng đổi lại giảm được gánh nặng review mã ngoài và mức độ phơi bày trước rủi ro chuỗi cung ứng. Dĩ nhiên thư viện lớn vẫn là vấn đề, nhưng cũng không thể tự viết mọi thứ. Đây không phải chuyện riêng của Rust mà là vấn đề chung
  • Trước đây tôi từng làm như vậy với các hệ thống quan trọng (ở ngôn ngữ khác): đặt chính sách tối thiểu hóa module/package, rồi chuyển mọi package sử dụng vào kho nội bộ, phân nhánh riêng và audit theo từng đợt cập nhật. Nhưng ở những mảng như frontend thì quản lý chặt đến thế gần như không khả thi. Gần đây, các công cụ và model AI mã nguồn mở ồn ào cũng đang vướng những băn khoăn tương tự về quản lý dependency. Khi làm dự án cá nhân bằng Rust, thứ khiến tôi ngại nhất cũng là sự bùng nổ dependency ở các thư viện UI/async. Chỉ cần một cái có lỗ hổng là bị đục thủng, và có lẽ đó chỉ là vấn đề thời gian
    • Cách thực tế là chỉ nối hệ thống CI/CD với kho nội bộ chính thức. Lập trình viên có thể cài gì ở local cũng được, nhưng commit không được phép sẽ bị chặn ở build server
    • Cũng có các RFC nhằm giải quyết rủi ro bảo mật, nhưng vì lý do văn hóa (có lẽ vậy) nên chưa có thay đổi mạnh mẽ
    • Điểm hay của Rust là ngay cả async cũng có thể tự triển khai theo cách mình muốn, không bị khóa vào một implementation cụ thể
 
iolothebard 2025-05-11

Đây không phải chỉ là vấn đề riêng của Rust.
Đó là ưu điểm chung đồng thời cũng là vấn đề tiềm ẩn của mọi ngôn ngữ có trình quản lý gói hỗ trợ kho lưu trữ gói công khai và phụ thuộc bắc cầu.
Cuối cùng thì vẫn là người mang về dùng phải dùng cho cho đúng thôi…
Dù đã có vụ việc leftpad của Node&npm, nhưng vẫn chẳng có gì thay đổi.