Bazel ट्यूटोरियल: Java प्रोजेक्ट बनाएं

इस ट्यूटोरियल में, Bazel की मदद से Java ऐप्लिकेशन बनाने की बुनियादी जानकारी दी गई है. इसमें आपको अपना वर्कस्पेस सेट अप करने और एक आसान Java प्रोजेक्ट बनाने के बारे में बताया जाएगा. इससे आपको Bazel के अहम कॉन्सेप्ट समझने में मदद मिलेगी. जैसे, टारगेट और BUILD फ़ाइलें.

पूरा होने में लगने वाला अनुमानित समय: 30 मिनट.

आपको क्या सीखने को मिलेगा

इस ट्यूटोरियल में, आपको इनके बारे में जानकारी मिलेगी:

  • कोई टारगेट बनाना
  • प्रोजेक्ट की डिपेंडेंसी देखना
  • प्रोजेक्ट को कई टारगेट और पैकेज में बांटना
  • पैकेज में टारगेट की विज़िबिलिटी कंट्रोल करना
  • लेबल की मदद से टारगेट का रेफ़रंस देना
  • कोई टारगेट डिप्लॉय करना

शुरू करने से पहले

Bazel इंस्टॉल करना

ट्यूटोरियल के लिए तैयारी करने के लिए, सबसे पहले Bazel इंस्टॉल करें. अगर यह पहले से इंस्टॉल है, तो इसे इंस्टॉल करने की ज़रूरत नहीं है.

JDK इंस्टॉल करना

  1. Java JDK इंस्टॉल करें. हमारा सुझाव है कि वर्शन 11 इंस्टॉल करें. हालांकि, वर्शन 8 से 15 भी इस्तेमाल किए जा सकते हैं.

  2. JAVA_HOME एनवायरमेंट वैरिएबल को JDK पर पॉइंट करने के लिए सेट करें.

    • Linux/macOS पर:

      export JAVA_HOME="$(dirname $(dirname $(realpath $(which javac))))"
      
    • Windows पर:

      1. कंट्रोल पैनल खोलें.
      2. "सिस्टम और सुरक्षा" > "सिस्टम" > "एडवांस सिस्टम सेटिंग" > "एडवांस" टैब > "एनवायरमेंट वैरिएबल..." पर जाएं .
      3. "यूज़र वैरिएबल" सूची (सबसे ऊपर वाली) में, "नया..." पर क्लिक करें.
      4. "वैरिएबल का नाम" फ़ील्ड में, JAVA_HOME डालें.
      5. "डायरेक्ट्री ब्राउज़ करें..." पर क्लिक करें.
      6. JDK डायरेक्ट्री पर जाएं. उदाहरण के लिए, C:\Program Files\Java\jdk1.8.0_152.
      7. सभी डायलॉग विंडो में "ठीक है" पर क्लिक करें.

सैंपल प्रोजेक्ट पाना

Bazel की GitHub रिपॉज़िटरी से सैंपल प्रोजेक्ट पाएं:

git clone https://github.com/bazelbuild/examples

इस ट्यूटोरियल के लिए सैंपल प्रोजेक्ट, examples/java-tutorial डायरेक्ट्री में है. इसका स्ट्रक्चर इस तरह है:

java-tutorial
├── BUILD
├── src
   └── main
       └── java
           └── com
               └── example
                   ├── cmdline
                      ├── BUILD
                      └── Runner.java
                   ├── Greeting.java
                   └── ProjectRunner.java
└── MODULE.bazel

Bazel की मदद से बनाना

वर्कस्पेस सेट अप करना

किसी प्रोजेक्ट को बनाने से पहले, आपको उसका वर्कस्पेस सेट अप करना होगा. वर्कस्पेस एक डायरेक्ट्री होती है, जिसमें आपके प्रोजेक्ट की सोर्स फ़ाइलें और Bazel के बिल्ड आउटपुट मौजूद होते हैं. इसमें ऐसी फ़ाइलें भी होती हैं जिन्हें Bazel खास तौर पर पहचानता है:

  • MODULE.bazel फ़ाइल. यह डायरेक्ट्री और उसके कॉन्टेंट को Bazel वर्कस्पेस के तौर पर पहचानती है. यह प्रोजेक्ट की डायरेक्ट्री स्ट्रक्चर के रूट में मौजूद होती है,

  • एक या उससे ज़्यादा BUILD फ़ाइलें. इनसे Bazel को पता चलता है कि प्रोजेक्ट के अलग-अलग हिस्सों को कैसे बनाना है. वर्कस्पेस में मौजूद कोई डायरेक्ट्री जिसमें BUILD फ़ाइल होती है, उसे पैकेज कहा जाता है. आपको इस ट्यूटोरियल में पैकेज के बारे में ज़्यादा जानकारी मिलेगी.)

किसी डायरेक्ट्री को Bazel वर्कस्पेस के तौर पर तय करने के लिए, उस डायरेक्ट्री में MODULE.bazel नाम की एक खाली फ़ाइल बनाएं.

Bazel की मदद से प्रोजेक्ट बनाते समय, सभी इनपुट और डिपेंडेंसी एक ही वर्कस्पेस में होनी चाहिए. अलग-अलग वर्कस्पेस में मौजूद फ़ाइलें एक-दूसरे से अलग होती हैं. हालांकि, इन्हें लिंक किया जा सकता है. इसके बारे में इस ट्यूटोरियल में नहीं बताया गया है.

BUILD फ़ाइल को समझना

BUILD फ़ाइल में, Bazel के लिए कई तरह के निर्देश होते हैं. इनमें सबसे अहम निर्देश बिल्ड रूल होता है. इससे Bazel को पता चलता है कि मनचाहे आउटपुट कैसे बनाने हैं. जैसे, एक्ज़ीक्यूटेबल बाइनरी या लाइब्रेरी. BUILD फ़ाइल में, बिल्ड रूल के हर इंस्टेंस को टारगेट कहा जाता है. यह सोर्स फ़ाइलों और डिपेंडेंसी के किसी खास सेट की ओर इशारा करता है. कोई टारगेट, दूसरे टारगेट की ओर भी इशारा कर सकता है.

java-tutorial/BUILD फ़ाइल देखें:

java_binary(
    name = "ProjectRunner",
    srcs = glob(["src/main/java/com/example/*.java"]),
)

हमारे उदाहरण में, ProjectRunner टारगेट, Bazel के बिल्ट-इन java_binary रूल को इंस्टैंशिएट करता है. इस रूल से Bazel को .jar फ़ाइल और रैपर शेल स्क्रिप्ट बनाने का निर्देश मिलता है. इन दोनों का नाम टारगेट के नाम पर रखा जाता है.

टारगेट में मौजूद एट्रिब्यूट से, उसकी डिपेंडेंसी और विकल्पों के बारे में साफ़ तौर पर पता चलता है. name एट्रिब्यूट ज़रूरी है, लेकिन कई एट्रिब्यूट ज़रूरी नहीं होते. उदाहरण के लिए, ProjectRunner रूल टारगेट में, name टारगेट का नाम है. srcs उन सोर्स फ़ाइलों के बारे में बताता है जिनका इस्तेमाल Bazel, टारगेट बनाने के लिए करता है. वहीं, main_class उस क्लास के बारे में बताता है जिसमें मुख्य तरीका शामिल होता है. (आपने देखा होगा कि हमारे उदाहरण में, सोर्स फ़ाइलों के सेट को Bazel पर पास करने के लिए, glob का इस्तेमाल किया गया है. ऐसा एक-एक करके सभी सोर्स फ़ाइलों को लिस्ट करने के बजाय किया गया है.)

प्रोजेक्ट बनाना

अपने सैंपल प्रोजेक्ट को बनाने के लिए, java-tutorial डायरेक्ट्री पर जाएं और यह निर्देश चलाएं:

bazel build //:ProjectRunner

टारगेट लेबल में, // हिस्सा, वर्कस्पेस के रूट के मुकाबले BUILD फ़ाइल की जगह है. इस मामले में, यह रूट ही है. वहीं, ProjectRunner, BUILD फ़ाइल में टारगेट का नाम है. (आपको इस ट्यूटोरियल के आखिर में, टारगेट लेबल के बारे में ज़्यादा जानकारी मिलेगी.)

Bazel, इस तरह का आउटपुट जनरेट करता है:

   INFO: Found 1 target...
   Target //:ProjectRunner up-to-date:
      bazel-bin/ProjectRunner.jar
      bazel-bin/ProjectRunner
   INFO: Elapsed time: 1.021s, Critical Path: 0.83s

बधाई हो, आपने Bazel का पहला टारगेट बना लिया है! Bazel, बिल्ड आउटपुट को वर्कस्पेस के रूट में मौजूद bazel-bin डायरेक्ट्री में रखता है. Bazel के आउटपुट स्ट्रक्चर के बारे में जानने के लिए, इसके कॉन्टेंट को ब्राउज़ करें.

अब नई बनी बाइनरी को टेस्ट करें:

bazel-bin/ProjectRunner

डिपेंडेंसी ग्राफ़ की समीक्षा करना

Bazel के लिए, BUILD फ़ाइलों में बिल्ड डिपेंडेंसी को साफ़ तौर पर बताने की ज़रूरत होती है. Bazel, प्रोजेक्ट का डिपेंडेंसी ग्राफ़ बनाने के लिए, इन स्टेटमेंट का इस्तेमाल करता है. इससे, सटीक इंक्रीमेंटल बिल्ड बनाए जा सकते हैं.

सैंपल प्रोजेक्ट की डिपेंडेंसी देखने के लिए, वर्कस्पेस के रूट पर यह निर्देश चलाकर, डिपेंडेंसी ग्राफ़ का टेक्स्ट वर्शन जनरेट किया जा सकता है:

bazel query  --notool_deps --noimplicit_deps "deps(//:ProjectRunner)" --output graph

ऊपर दिए गए निर्देश से Bazel को, टारगेट //:ProjectRunner के लिए सभी डिपेंडेंसी (होस्ट और इंप्लिसिट डिपेंडेंसी को छोड़कर) ढूंढने और आउटपुट को ग्राफ़ के तौर पर फ़ॉर्मैट करने का निर्देश मिलता है.

इसके बाद, टेक्स्ट को GraphViz में चिपकाएं.

जैसा कि देखा जा सकता है, प्रोजेक्ट में एक ही टारगेट है. इससे दो सोर्स फ़ाइलें बनती हैं. इसमें कोई अतिरिक्त डिपेंडेंसी नहीं है:

टारगेट 'ProjectRunner' का डिपेंडेंसी ग्राफ़

वर्कस्पेस सेट अप करने, प्रोजेक्ट बनाने, और उसकी डिपेंडेंसी की जांच करने के बाद, इसमें कुछ जटिलता जोड़ी जा सकती है.

Bazel बिल्ड को बेहतर बनाना

छोटे प्रोजेक्ट के लिए एक टारगेट काफ़ी होता है. हालांकि, बड़े प्रोजेक्ट को कई टारगेट और पैकेज में बांटा जा सकता है. इससे, इंक्रीमेंटल बिल्ड तेज़ी से बनाए जा सकते हैं. इसका मतलब है कि सिर्फ़ उन हिस्सों को फिर से बनाया जा सकता है जिनमें बदलाव किया गया है. साथ ही, प्रोजेक्ट के कई हिस्सों को एक साथ बनाकर, बिल्ड की प्रोसेस को तेज़ किया जा सकता है.

एक से ज़्यादा बिल्ड टारगेट तय करना

सैंपल प्रोजेक्ट के बिल्ड को दो टारगेट में बांटा जा सकता है. java-tutorial/BUILD फ़ाइल के कॉन्टेंट को इससे बदलें:

java_binary(
    name = "ProjectRunner",
    srcs = ["src/main/java/com/example/ProjectRunner.java"],
    main_class = "com.example.ProjectRunner",
    deps = [":greeter"],
)

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
)

इस कॉन्फ़िगरेशन के साथ, Bazel सबसे पहले greeter लाइब्रेरी बनाता है. इसके बाद, ProjectRunner बाइनरी बनाता है. java_binary में मौजूद deps एट्रिब्यूट से Bazel को पता चलता है कि ProjectRunner बाइनरी बनाने के लिए, greeter लाइब्रेरी की ज़रूरत होती है.

प्रोजेक्ट के इस नए वर्शन को बनाने के लिए, यह निर्देश चलाएं:

bazel build //:ProjectRunner

Bazel, इस तरह का आउटपुट जनरेट करता है:

INFO: Found 1 target...
Target //:ProjectRunner up-to-date:
  bazel-bin/ProjectRunner.jar
  bazel-bin/ProjectRunner
INFO: Elapsed time: 2.454s, Critical Path: 1.58s

अब नई बनी बाइनरी को टेस्ट करें:

bazel-bin/ProjectRunner

अगर अब ProjectRunner.java में बदलाव किया जाता है और प्रोजेक्ट को फिर से बनाया जाता है, तो Bazel सिर्फ़ उस फ़ाइल को फिर से कंपाइल करता है.

डिपेंडेंसी ग्राफ़ को देखने पर पता चलता है कि ProjectRunner पहले की तरह ही इनपुट पर निर्भर करता है. हालांकि, बिल्ड का स्ट्रक्चर अलग है:

डिपेंडेंसी जोड़ने के बाद, टारगेट 'ProjectRunner' का डिपेंडेंसी ग्राफ़

अब आपने दो टारगेट के साथ प्रोजेक्ट बना लिया है. ProjectRunner टारगेट, एक सोर्स फ़ाइल बनाता है और एक अन्य टारगेट (:greeter) पर निर्भर करता है. यह एक अतिरिक्त सोर्स फ़ाइल बनाता है.

एक से ज़्यादा पैकेज इस्तेमाल करना

अब प्रोजेक्ट को कई पैकेज में बांटते हैं. src/main/java/com/example/cmdline डायरेक्ट्री को देखने पर पता चलता है कि इसमें BUILD फ़ाइल के साथ-साथ कुछ सोर्स फ़ाइलें भी मौजूद हैं. इसलिए, Bazel के लिए, वर्कस्पेस में अब दो पैकेज हैं: //src/main/java/com/example/cmdline और //. ऐसा इसलिए है, क्योंकि वर्कस्पेस के रूट में एक BUILD फ़ाइल मौजूद है.

src/main/java/com/example/cmdline/BUILD फ़ाइल देखें:

java_binary(
    name = "runner",
    srcs = ["Runner.java"],
    main_class = "com.example.cmdline.Runner",
    deps = ["//:greeter"],
)

runner टारगेट, // पैकेज में मौजूद greeter टारगेट पर निर्भर करता है. इसलिए टारगेट लेबल //:greeter है. Bazel को इसकी जानकारी deps एट्रिब्यूट से मिलती है. डिपेंडेंसी ग्राफ़ देखें:

टारगेट 'रनर' का डिपेंडेंसी ग्राफ़

हालांकि, बिल्ड को पूरा करने के लिए, आपको visibility एट्रिब्यूट का इस्तेमाल करके, //src/main/java/com/example/cmdline/BUILD में मौजूद runner टारगेट को, //BUILD में मौजूद टारगेट के लिए साफ़ तौर पर विज़िबिलिटी देनी होगी. ऐसा इसलिए है, क्योंकि डिफ़ॉल्ट रूप से टारगेट सिर्फ़ उसी BUILD फ़ाइल में मौजूद अन्य टारगेट को दिखते हैं. (Bazel, टारगेट की विज़िबिलिटी का इस्तेमाल करके, उन समस्याओं को रोकता है जिनमें लाइब्रेरी में मौजूद इंप्लीमेंटेशन की जानकारी, सार्वजनिक एपीआई में लीक हो जाती है.)

इसके लिए, java-tutorial/BUILD में मौजूद greeter टारगेट में visibility एट्रिब्यूट जोड़ें. यह एट्रिब्यूट इस तरह जोड़ा जाता है:

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
    visibility = ["//src/main/java/com/example/cmdline:__pkg__"],
)

अब वर्कस्पेस के रूट पर यह निर्देश चलाकर, नया पैकेज बनाया जा सकता है:

bazel build //src/main/java/com/example/cmdline:runner

Bazel, इस तरह का आउटपुट जनरेट करता है:

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner.jar
  bazel-bin/src/main/java/com/example/cmdline/runner
  INFO: Elapsed time: 1.576s, Critical Path: 0.81s

अब नई बनी बाइनरी को टेस्ट करें:

./bazel-bin/src/main/java/com/example/cmdline/runner

अब आपने प्रोजेक्ट में बदलाव करके, इसे दो पैकेज के तौर पर बनाया है. हर पैकेज में एक टारगेट है. साथ ही, आपने इनके बीच की डिपेंडेंसी को भी समझ लिया है.

टारगेट का रेफ़रंस देने के लिए लेबल का इस्तेमाल करना

BUILD फ़ाइलों और कमांड लाइन पर, Bazel, टारगेट का रेफ़रंस देने के लिए टारगेट लेबल का इस्तेमाल करता है. उदाहरण के लिए, //:ProjectRunner या //src/main/java/com/example/cmdline:runner. इनका सिंटैक्स इस तरह है:

//path/to/package:target-name

अगर टारगेट, रूल टारगेट है, तो path/to/package उस डायरेक्ट्री का पाथ है जिसमें BUILD फ़ाइल मौजूद है. वहीं, target-name, BUILD फ़ाइल में टारगेट का नाम है. यह name एट्रिब्यूट होता है. अगर टारगेट, फ़ाइल टारगेट है, तो path/to/package, पैकेज के रूट का पाथ है. वहीं, target-name, टारगेट फ़ाइल का नाम है. इसमें उसका पूरा पाथ भी शामिल होता है.

रिपॉज़िटरी के रूट पर टारगेट का रेफ़रंस देते समय, पैकेज का पाथ खाली होता है. इसके लिए, सिर्फ़ //:target-name का इस्तेमाल करें. एक ही BUILD फ़ाइल में मौजूद टारगेट का रेफ़रंस देते समय, // वर्कस्पेस रूट आइडेंटिफ़ायर को भी छोड़ा जा सकता है. इसके लिए, सिर्फ़ :target-name का इस्तेमाल करें.

उदाहरण के लिए, java-tutorial/BUILD फ़ाइल में मौजूद टारगेट के लिए, आपको पैकेज का पाथ तय करने की ज़रूरत नहीं पड़ी. ऐसा इसलिए है, क्योंकि वर्कस्पेस का रूट ही एक पैकेज (//) है. साथ ही, आपके दो टारगेट लेबल सिर्फ़ //:ProjectRunner और //:greeter थे.

हालांकि, //src/main/java/com/example/cmdline/BUILD फ़ाइल में मौजूद टारगेट के लिए, आपको //src/main/java/com/example/cmdline का पूरा पैकेज पाथ तय करना पड़ा. साथ ही, आपका टारगेट लेबल //src/main/java/com/example/cmdline:runner था.

डिप्लॉयमेंट के लिए Java टारगेट को पैकेज करना

अब डिप्लॉयमेंट के लिए, Java टारगेट को पैकेज करते हैं. इसके लिए, बाइनरी को उसकी सभी रनटाइम डिपेंडेंसी के साथ बनाते हैं. इससे, डेवलपमेंट एनवायरमेंट के बाहर बाइनरी को चलाया जा सकता है.

जैसा कि आपको याद होगा, java_binary बिल्ड रूल एक .jar और रैपर शेल स्क्रिप्ट बनाता है. यह निर्देश इस्तेमाल करके, runner.jar का कॉन्टेंट देखें:

jar tf bazel-bin/src/main/java/com/example/cmdline/runner.jar

इसका कॉन्टेंट यह है:

META-INF/
META-INF/MANIFEST.MF
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class

जैसा कि देखा जा सकता है, runner.jar में Runner.class मौजूद है. हालांकि, इसकी डिपेंडेंसी Greeting.class मौजूद नहीं है. Bazel से जनरेट की गई runner स्क्रिप्ट, क्लासपाथ में greeter.jar जोड़ती है. इसलिए, अगर इसे ऐसे ही छोड़ दिया जाता है, तो यह स्थानीय तौर पर चलेगा. हालांकि, इसे किसी अन्य मशीन पर स्टैंडअलोन नहीं चलाया जा सकेगा. अच्छी बात यह है कि java_binary रूल की मदद से, डिप्लॉय की जा सकने वाली, सेल्फ-कंटेन्ड बाइनरी बनाई जा सकती है. इसे बनाने के लिए, टारगेट के नाम में _deploy.jar जोड़ें:

bazel build //src/main/java/com/example/cmdline:runner_deploy.jar

Bazel, इस तरह का आउटपुट जनरेट करता है:

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner_deploy.jar up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar
INFO: Elapsed time: 1.700s, Critical Path: 0.23s

आपने अभी runner_deploy.jar बनाया है. इसे अपने डेवलपमेंट एनवायरमेंट से बाहर, स्टैंडअलोन चलाया जा सकता है. ऐसा इसलिए है, क्योंकि इसमें ज़रूरी रनटाइम डिपेंडेंसी मौजूद हैं. इस स्टैंडअलोन JAR का कॉन्टेंट देखने के लिए, पहले वाला ही निर्देश इस्तेमाल करें:

jar tf bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar

इसके कॉन्टेंट में, चलाने के लिए ज़रूरी सभी क्लास शामिल हैं:

META-INF/
META-INF/MANIFEST.MF
build-data.properties
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class
com/example/Greeting.class

इस बारे में और पढ़ें

ज़्यादा जानकारी के लिए, ये लेख पढ़ें:

बिल्डिंग के लिए शुभकामनाएं!