- Rust का dependency management system development को सुविधाजनक बनाता है, लेकिन dependencies की संख्या और उनकी quality अब चिंता का विषय है
- जिन Crate का अच्छा-खासा इस्तेमाल होता है, वे भी ज़रूरी नहीं कि up-to-date हों; कई बार उन्हें खुद implement करना बेहतर हो सकता है
- Axum, Tokio जैसे मशहूर Crate जोड़ने के बाद, dependencies सहित पूरे codebase की lines of code 36 लाख तक पहुँच गईं, जिसे संभालना मुश्किल है
- मैंने वास्तव में जो code लिखा है वह सिर्फ़ लगभग 1,000 lines का है, लेकिन आसपास के code की review और audit करना practically संभव नहीं है
- Rust standard library को बढ़ाने या core infrastructure को कैसे implement किया जाए, इसका कोई स्पष्ट solution नहीं है; community को performance, safety और maintainability के बीच संतुलन पर मिलकर सोचना होगा
Rust dependency समस्या का अवलोकन
- Rust मेरी सबसे पसंदीदा language है, और इसकी community और usability बेहद शानदार हैं
- development productivity ऊँची है, लेकिन हाल के समय में dependency management को लेकर चिंता बढ़ी है
Rust Crate और Cargo के फ़ायदे
- Cargo के ज़रिए package management और build automation संभव है, जिससे productivity बहुत बढ़ जाती है
- अलग-अलग operating systems और architectures के बीच जाना आसान हो जाता है, और files को manually manage करने या build tools configure करने की ज़रूरत नहीं पड़ती
- अलग से package management की चिंता किए बिना सीधे code लिखना शुरू किया जा सकता है
Rust Crate management के नुकसान
- package management पर कम ध्यान देने की वजह से stability पर भी कम ध्यान रह जाता है
- उदाहरण के लिए, मैंने dotenv crate इस्तेमाल किया, लेकिन बाद में Security Advisory से पता चला कि उसका maintenance बंद हो चुका है
- विकल्प crate (dotenvy) पर विचार करने के बजाय, मैंने जो हिस्सा वास्तव में चाहिए था उसे लगभग 35 lines में खुद implement कर लिया
- कई languages में packages के unmaintained हो जाने की समस्या बार-बार आती है, इसलिए असली समस्या dependency पर अनिवार्य निर्भरता है
Dependencies से code की मात्रा में तेज़ बढ़ोतरी
- मैं Tokio, Axum जैसे Rust ecosystem के महत्वपूर्ण और high-quality packages इस्तेमाल कर रहा हूँ
- dependency के रूप में Axum, Reqwest, ripunzip, serde, serde_json, tokio, tower-http, tracing, tracing-subscriber जोड़े गए
- मुख्य उद्देश्य web server, file extraction और logging है, इसलिए project ख़ुद काफ़ी simple है
- Cargo vendor feature का उपयोग करके सभी dependent crates को locally download किया गया
- tokei से lines of code का analysis करने पर, dependencies सहित यह संख्या लगभग 36 लाख lines निकली (vendored crates को छोड़ने पर लगभग 11,136 lines)
- तुलना के लिए, पूरा Linux kernel लगभग 2.78 करोड़ lines का माना जाता है; यानी मेरा छोटा-सा project भी उसका करीब सातवाँ हिस्सा बन जाता है
- मेरे द्वारा लिखा गया वास्तविक code सिर्फ़ लगभग 1,000 lines का है
- इतनी बड़ी मात्रा में dependency code की निगरानी और audit करना practically असंभव है
समाधान को लेकर विचार
- फिलहाल कोई स्पष्ट समाधान नज़र नहीं आता
- कुछ लोग Go की तरह standard library का विस्तार करने की बात करते हैं, लेकिन इससे maintenance burden जैसी नई समस्याएँ भी पैदा होती हैं
- Rust का लक्ष्य high performance, safety और modularity है, और यह embedded systems या C++ से प्रतिस्पर्धा करना चाहता है, इसलिए standard library को बढ़ाने में सावधानी ज़रूरी है
- उदाहरण के लिए, Tokio जैसा उन्नत runtime भी Github और Discord पर बहुत सक्रिय रूप से maintain किया जाता है
- व्यवहारिक रूप से async runtime या web server जैसे core infrastructure को खुद implement करना किसी individual developer के लिए बहुत कठिन है
- बड़ी service Cloudflare भी tokio और crates.io dependencies का वैसे ही उपयोग करती है, लेकिन वह audit कितनी बार करती है, यह स्पष्ट नहीं है
- Clickhouse ने भी binary size और crates की संख्या से जुड़ी समस्याओं का उल्लेख किया है
- Cargo के साथ final binary में शामिल होने वाली lines of code को सटीक रूप से पहचानना मुश्किल है, और platform-specific अनावश्यक code भी शामिल हो सकता है
- अंत में, पूरी community से ही जवाब माँगना पड़ रहा है
3 टिप्पणियां
Trivy चलाकर देखें तो js NPM या Java Maven की तुलना में high या critical काफ़ी कम हैं और ज़्यादा सुरक्षित लगता है, तो यह लेख Rust के बारे में आखिर क्या कहना चाहता है?
Hacker News की राय
import foolibकी एक लाइन से काम हो जाता है, और उसके अंदर क्या है इसकी किसी को परवाह नहीं होती। हर स्तर पर शायद सिर्फ़ 5% functionality चाहिए होती है, लेकिन tree जितना गहरा होता है, बेकार code उतना जमा होता जाता है। नतीजा यह कि एक साधारण binary 500MiB की हो जाती है, और सिर्फ़ number formatting के लिए भी dependency खींच ली जाती है। Go या Rust सब कुछ एक ही फ़ाइल में ठूँसने को बढ़ावा देते हैं, इसलिए अगर आप सिर्फ़ कुछ हिस्सा इस्तेमाल करना चाहें तो मुश्किल हो जाती है। लंबी अवधि में असली समाधान ultra-fine-grained symbol/dependency tracking है, जहाँ हर function/type सिर्फ़ वही ज़रूरी तत्व घोषित करे, जिससे बिल्कुल आवश्यक code ही लिया जाए और बाकी फेंक दिया जाए। मुझे व्यक्तिगत रूप से यह विचार पसंद नहीं है, लेकिन dependency tree से पूरा ब्रह्मांड खींच लाने वाली मौजूदा व्यवस्था का समाधान मुझे इसी में दिखता हैserde_jsonको ही छोटे बदलाव से हटाया जा सका। बड़ी dependencies (winit/wgpuआदि) हटाने के लिए architecture बदलना पड़ता, इसलिए उन्हें आसानी से नहीं हटाया जा सकता.ofile बनती थी और उन्हें.aarchive में बाँधा जाता था, फिर linker सिर्फ़ ज़रूरी function निकालकर इस्तेमाल करता था। namespacing भीfoolib_do_thing()जैसे तरीके से होती थी। अबgod objectpattern की तरह top-level object में सारे functions रख दिए जाते हैं, इसलिएfoolibimport करते ही सब कुछ खिंच आता है। ऐसी स्थिति में linker के लिए यह तय करना कठिन हो जाता है कि कौन-सा function वास्तव में ज़रूरी है। इसके उलट Go dead code elimination बहुत अच्छी तरह करता है, इसलिए जो इस्तेमाल नहीं होता वह compiled output से कट जाता हैmin-sized-rustजैसे projects के ज़रिए इसे support करता हैdepsfile में एक लाइन जोड़ने की तुलना में यह कहीं ज़्यादा गहरा संवाद पैदा करता थाisEven,isOdd,leftpadजैसी छोटी-छोटी library pieces की distributed maintenance से बेहतर है कि किसी federated team द्वारा managed बड़ी general-purpose library हो, क्योंकि वह भविष्य-स्थिरता और continuity कहीं ज़्यादा देती है--gc-sectionsजैसी section splitting के ज़रिए हल हो रहा हैgloblibrary ideally सिर्फ़ एक साधारण globbing function होनी चाहिए, लेकिन author ने उसके साथ command-line tool भी बाँध दिया और एक बड़ा parser dependency के रूप में जोड़ दिया। इससे बार-बार "dependency out-of-date" warnings आती हैं। साथ हीgloblibrary की responsibility boundary भी विवाद का विषय है। सिर्फ़ string pattern matching करना अधिक flexible design है (testing या filesystem abstraction के लिए आसान)। कई users एक सर्वशक्तिमान "Do everything" library चाहते हैं, लेकिन ऐसा करने पर side effects भी बढ़ते हैं। मुझे लगता है Rust भी इससे बहुत अलग नहीं होगाstdlib::data_structures::automata::weighted_finite_state_transducerआदि) और सुव्यवस्थित namespaces के साथ "batteries included" configuration हो। language के अंदर ही version management और backward compatibility built-in है, इसलिए भविष्य में सुधार की उम्मीद हैglobfunction वास्तव में filesystem को traverse करता है। string matching के लिएfnmatchहै। आदर्श रूप सेfnmatchअलग module में हो औरglobउसकी dependency हो। अगरglobको सीधे implement करना चाहें तो यह काफ़ी कठिन है, क्योंकि directory structure, brace expansion जैसी जटिल requirements होती हैं, इसलिए अच्छी तरह डिज़ाइन किए गए functions के संयोजन की ज़रूरत होती हैglobfunctionality शामिल हैcapability systembuilt-in होना चाहिए ताकि पूरे library tree को सुरक्षित रूप से isolate किया जा सके। उदाहरण के लिए image loading library डिज़ाइन करते समय, उसे files की जगह सिर्फ़ stream लेने वाली बनाया जाए, या साफ़-साफ़ बताया जाए कि उसे "file खोलने की permission नहीं है", ताकि ख़तरनाक functions का उपयोग compile time पर रोका जा सके। मौजूदा ecosystems में यह आसान नहीं है, लेकिन सही ढंग से किया जाए तो attack surface कम किया जा सकता है। सिर्फ़ dependency minimization culture से मूल समस्या हल नहीं होती, और Go जैसी languages भी supply-chain attacks से मुक्त नहीं हैंHello worldतक print नहीं कर सकती, उसमें string type भी नहीं है। लेकिन वह untrusted file format parsing के लिए विशेष रूप से बनी है। ऐसी special-purpose languages और होनी चाहिए। वे तेज़ भी हैं, जोखिम भी कम है, इसलिए अनावश्यक checks भी घटते हैं#![deny(unsafe_code)]के ज़रिए unsafe code इस्तेमाल होने पर compile error रोकी जा सकती है और यह बात user को बताई जा सकती है। हालाँकि यह पूर्ण enforcement नहीं है; विशेष अनुमति देकर unsafe code फिर भी इस्तेमाल किया जा सकता है। कल्पना की जा सकती है कि standard library की functionality को transitive रूप से नियंत्रित करने वाला कोई capability system,featureflag की तरह, जोड़ा जाएpanicकराया जाए, और हर library के लिए capability profile लिखने/वितरित करने का प्रयास हो। TypeScript में इस तरह की कुछ चीज़ें पहले ही साबित हुई हैंdom0में हो, हर library अलग template VM में हो, और communication network namespaces से हो। sensitive industries में यह व्यावहारिक हो सकता हैblessed.rsstandard library में शामिल करना कठिन लेकिन उपयोगी libraries की सूची recommend करता है। मुझे यह व्यवस्था पसंद है क्योंकि इसकी वजह से अधिकांश packages किसी विशेष उद्देश्य तक सीमित रहते हैं और manage किए जा सकते हैंcargo-vetभी recommend करने लायक है। यह trusted packages को track/define करने देता है; जैसे कुछ packages के लिए import से पहले expert audit ज़रूरी हो, जबकिtokiomaintainers द्वारा managed packages को सीधा trust कर लिया जाए — ऐसे semi-YOLO policies तक संभव हैं। यहblessed.rsसे थोड़ा ज़्यादा formal है, और टीम के भीतर एक official quasi-standard list साझा करने के लिए अच्छा माध्यम हैleftpadघटना के बाद package managers के प्रति नकारात्मक धारणा बची हुई है।tokioजैसी चीज़ें तो लगभग language-level functionality हैं, इसलिए अगर OP यह कह रहा है कि Go के पूरे ecosystem या Node के V8 तक का भी direct audit करना चाहिए, तो यह अव्यावहारिक हैtokioका भी कोई न कोई लगातार audit करता है। बहुत लोग नहीं करते, लेकिन कोई तो करता ही हैfeatureflag वास्तव में बहुत अच्छी चीज़ है। मैं अक्सर ऐसे PR डालता हूँ जो अनावश्यक dependencies को इन flags के पीछे छिपा देते हैं।cargo treeसे dependency tree आसानी से देखा जा सकता है। binary में वास्तव में जाने वाली code lines का view बहुत मायने नहीं रखता, क्योंकि function inlining होने पर ज़्यादातर चीज़ेंmainमें मिल जाती हैंयह सिर्फ Rust की ही समस्या नहीं है.
public package repository और transitive dependency को support करने वाले package manager वाली हर language का यह एक साझा फ़ायदा भी है और संभावित समस्या भी.
आखिरकार, इन्हें लाकर इस्तेमाल करने वालों को ही सही तरीके से इस्तेमाल करना चाहिए…
Node&npm के leftpad मामले के बावजूद कुछ भी नहीं बदला.