বেজেল কোড বেস

এই নথিটি কোড বেস এবং কিভাবে Bazel গঠন করা হয় তার একটি বিবরণ। এটি Bazel-এ অবদান রাখতে ইচ্ছুক ব্যক্তিদের জন্য, শেষ ব্যবহারকারীদের জন্য নয়।

ভূমিকা

Bazel এর কোড বেস বড় (~350KLOC প্রোডাকশন কোড এবং ~260 KLOC টেস্ট কোড) এবং কেউই পুরো ল্যান্ডস্কেপের সাথে পরিচিত নয়: প্রত্যেকেই তাদের নির্দিষ্ট উপত্যকাটি খুব ভালভাবে জানে, কিন্তু খুব কম লোকই জানে যে প্রতিটি দিকে পাহাড়ের উপরে কী রয়েছে।

যাত্রার মাঝপথে মানুষ যাতে সরল পথ হারিয়ে যাওয়ার সাথে সাথে বনের অন্ধকারের মধ্যে নিজেকে খুঁজে না পায়, এই নথিটি কোড বেসের একটি ওভারভিউ দেওয়ার চেষ্টা করে যাতে এটিতে কাজ শুরু করা সহজ হয়।

Bazel-এর সোর্স কোডের সর্বজনীন সংস্করণ GitHub-এ github.com/bazelbuild/bazel- এ থাকে। এটি "সত্যের উত্স" নয়; এটি একটি Google-অভ্যন্তরীণ উৎস ট্রি থেকে উদ্ভূত যা অতিরিক্ত কার্যকারিতা ধারণ করে যা Google-এর বাইরে উপযোগী নয়। দীর্ঘমেয়াদী লক্ষ্য হল গিটহাবকে সত্যের উৎস করা।

অবদানগুলি নিয়মিত GitHub পুল অনুরোধ প্রক্রিয়ার মাধ্যমে গৃহীত হয় এবং একটি Googler দ্বারা অভ্যন্তরীণ উত্স ট্রিতে ম্যানুয়ালি আমদানি করা হয়, তারপর GitHub-এ পুনরায় রপ্তানি করা হয়।

ক্লায়েন্ট/সার্ভার আর্কিটেকচার

Bazel এর বেশিরভাগ অংশ একটি সার্ভার প্রক্রিয়ায় থাকে যা বিল্ডগুলির মধ্যে RAM-তে থাকে। এটি বাজেলকে বিল্ডগুলির মধ্যে অবস্থা বজায় রাখার অনুমতি দেয়।

এই কারণেই Bazel কমান্ড লাইনে দুটি ধরণের বিকল্প রয়েছে: স্টার্টআপ এবং কমান্ড। এই মত একটি কমান্ড লাইনে:

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

কিছু অপশন ( --host_jvm_args= ) কমান্ডের নামের আগে এবং কিছু পরে ( -c opt ); আগের ধরনেরটিকে "স্টার্টআপ বিকল্প" বলা হয় এবং সার্ভার প্রক্রিয়াকে সম্পূর্ণভাবে প্রভাবিত করে, যেখানে পরবর্তী ধরনের, "কমান্ড বিকল্প", শুধুমাত্র একটি একক কমান্ডকে প্রভাবিত করে।

প্রতিটি সার্ভার ইনস্ট্যান্সে একটি একক সংশ্লিষ্ট সোর্স ট্রি ("ওয়ার্কস্পেস") থাকে এবং প্রতিটি ওয়ার্কস্পেসে সাধারণত একটি একক সক্রিয় সার্ভার ইনস্ট্যান্স থাকে। একটি কাস্টম আউটপুট বেস নির্দিষ্ট করে এটিকে বাধা দেওয়া যেতে পারে (আরো তথ্যের জন্য "ডিরেক্টরি লেআউট" বিভাগটি দেখুন)।

Bazel একটি একক ELF এক্সিকিউটেবল হিসাবে বিতরণ করা হয় যা একটি বৈধ .zip ফাইল। আপনি যখন bazel টাইপ করেন, C++ ("ক্লায়েন্ট") এ প্রয়োগ করা উপরের ELF এক্সিকিউটেবল নিয়ন্ত্রণ পায়। এটি নিম্নলিখিত পদক্ষেপগুলি ব্যবহার করে একটি উপযুক্ত সার্ভার প্রক্রিয়া সেট আপ করে:

  1. এটি ইতিমধ্যে নিজেই নিষ্কাশন করেছে কিনা তা পরীক্ষা করে। যদি না হয়, এটা করে। সার্ভারের বাস্তবায়ন এখান থেকে আসে।
  2. একটি সক্রিয় সার্ভার উদাহরণ কাজ করে কিনা তা পরীক্ষা করে: এটি চলছে, এটিতে সঠিক স্টার্টআপ বিকল্প রয়েছে এবং সঠিক ওয়ার্কস্পেস ডিরেক্টরি ব্যবহার করে। এটি $OUTPUT_BASE/server ডিরেক্টরিটি দেখে চলমান সার্ভারটি খুঁজে পায় যেখানে সার্ভারটি যে পোর্টটি শুনছে তার সাথে একটি লক ফাইল রয়েছে।
  3. প্রয়োজন হলে, পুরানো সার্ভার প্রক্রিয়া হত্যা করে
  4. প্রয়োজন হলে, একটি নতুন সার্ভার প্রক্রিয়া শুরু করে

একটি উপযুক্ত সার্ভার প্রক্রিয়া প্রস্তুত হওয়ার পরে, যে কমান্ডটি চালানোর প্রয়োজন তা একটি জিআরপিসি ইন্টারফেসের মাধ্যমে এটিতে যোগাযোগ করা হয়, তারপর বেজেলের আউটপুট টার্মিনালে ফিরে আসে। একই সময়ে শুধুমাত্র একটি কমান্ড চলতে পারে। এটি C++ এর অংশ এবং জাভাতে অংশ সহ একটি বিস্তৃত লকিং প্রক্রিয়া ব্যবহার করে প্রয়োগ করা হয়। সমান্তরালে একাধিক কমান্ড চালানোর জন্য কিছু অবকাঠামো রয়েছে, যেহেতু অন্য কমান্ডের সাথে সমান্তরালে bazel version চালানোর অক্ষমতা কিছুটা বিব্রতকর। প্রধান ব্লকার হল BlazeRuntime এর জীবনচক্র এবং BlazeModule এর কিছু অবস্থা।

একটি কমান্ডের শেষে, Bazel সার্ভার ক্লায়েন্টকে ফেরত দেওয়া প্রস্থান কোড প্রেরণ করে। একটি আকর্ষণীয় বলি হ'ল bazel run বাস্তবায়ন: এই কমান্ডের কাজ হল ব্যাজেল সদ্য নির্মিত কিছু চালানো, তবে এটি সার্ভার প্রক্রিয়া থেকে এটি করতে পারে না কারণ এটির একটি টার্মিনাল নেই। তাই পরিবর্তে এটি ক্লায়েন্টকে বলে যে এটি কোন বাইনারি ujexec() এবং কোন আর্গুমেন্টের সাথে করা উচিত।

যখন কেউ Ctrl-C চাপে, ক্লায়েন্ট এটিকে জিআরপিসি সংযোগে একটি বাতিল কলে অনুবাদ করে, যা যত তাড়াতাড়ি সম্ভব কমান্ডটি বন্ধ করার চেষ্টা করে। তৃতীয় Ctrl-C এর পরে, ক্লায়েন্ট পরিবর্তে সার্ভারে একটি SIGKILL পাঠায়।

ক্লায়েন্টের সোর্স কোড src/main/cpp এর অধীনে এবং সার্ভারের সাথে যোগাযোগের জন্য ব্যবহৃত প্রোটোকলটি src/main/protobuf/command_server.proto

সার্ভারের প্রধান এন্ট্রি পয়েন্ট হল BlazeRuntime.main() এবং ক্লায়েন্ট থেকে আসা gRPC কলগুলি GrpcServerImpl.run() দ্বারা পরিচালিত হয়।

ডিরেক্টরি বিন্যাস

Bazel একটি বিল্ড করার সময় ডিরেক্টরিগুলির একটি কিছুটা জটিল সেট তৈরি করে। আউটপুট ডিরেক্টরি বিন্যাসে একটি সম্পূর্ণ বিবরণ পাওয়া যায়।

"ওয়ার্কস্পেস" হল সোর্স ট্রি বাজেল চালানো হয়। এটি সাধারণত সোর্স কন্ট্রোল থেকে চেক আউট করা কিছুর সাথে মিলে যায়।

ব্যাজেল তার সমস্ত ডেটা "আউটপুট ব্যবহারকারী রুট" এর অধীনে রাখে। এটি সাধারণত $HOME/.cache/bazel/_bazel_${USER} হয়, কিন্তু --output_user_root স্টার্টআপ বিকল্প ব্যবহার করে ওভাররাইড করা যেতে পারে।

"ইনস্টল বেস" হল যেখানে Bazel বের করা হয়। এটি স্বয়ংক্রিয়ভাবে সম্পন্ন হয় এবং প্রতিটি Bazel সংস্করণ ইনস্টল বেসের অধীনে তার চেকসামের উপর ভিত্তি করে একটি সাবডিরেক্টরি পায়। এটি ডিফল্টরূপে $OUTPUT_USER_ROOT/install এ রয়েছে এবং --install_base কমান্ড লাইন বিকল্প ব্যবহার করে পরিবর্তন করা যেতে পারে।

"আউটপুট বেস" হল সেই জায়গা যেখানে একটি নির্দিষ্ট ওয়ার্কস্পেসের সাথে সংযুক্ত Bazel ইন্সট্যান্স লেখে। প্রতিটি আউটপুট বেস সর্বোচ্চ একটি Bazel সার্ভার দৃষ্টান্ত যে কোনো সময়ে চলমান আছে. এটি সাধারণত $OUTPUT_USER_ROOT/<checksum of the path to the workspace> । এটি --output_base স্টার্টআপ বিকল্প ব্যবহার করে পরিবর্তন করা যেতে পারে, যা অন্যান্য জিনিসের মধ্যে, যে কোনো নির্দিষ্ট সময়ে যে কোনো ওয়ার্কস্পেসে শুধুমাত্র একটি Bazel ইন্সট্যান্স চলতে পারে এমন সীমাবদ্ধতা অতিক্রম করার জন্য উপযোগী।

আউটপুট ডিরেক্টরিতে অন্যান্য জিনিসের মধ্যে রয়েছে:

  • $OUTPUT_BASE/external এ বহিরাগত সংগ্রহস্থলগুলি আনা হয়েছে।
  • exec রুট, একটি ডিরেক্টরি যাতে বর্তমান বিল্ডের জন্য সমস্ত সোর্স কোডের সিমলিঙ্ক থাকে। এটি $OUTPUT_BASE/execroot এ অবস্থিত। বিল্ড করার সময়, কাজের ডিরেক্টরি হল $EXECROOT/<name of main repository> । আমরা এটিকে $EXECROOT এ পরিবর্তন করার পরিকল্পনা করছি, যদিও এটি একটি দীর্ঘমেয়াদী পরিকল্পনা কারণ এটি একটি খুব বেমানান পরিবর্তন।
  • নির্মাণের সময় নির্মিত ফাইল।

একটি কমান্ড কার্যকর করার প্রক্রিয়া

একবার Bazel সার্ভার নিয়ন্ত্রণ পায় এবং এটি কার্যকর করার জন্য একটি কমান্ড সম্পর্কে অবহিত করা হয়, নিম্নলিখিত ঘটনাগুলির ক্রম ঘটে:

  1. BlazeCommandDispatcher নতুন অনুরোধ সম্পর্কে অবহিত করা হয়. এটি নির্ধারণ করে যে কমান্ডটি চালানোর জন্য একটি ওয়ার্কস্পেস প্রয়োজন কিনা (প্রায় প্রতিটি কমান্ড ছাড়া যেগুলি সোর্স কোডের সাথে কিছু করার নেই, যেমন সংস্করণ বা সাহায্য) এবং অন্য কমান্ড চলছে কিনা।

  2. সঠিক আদেশ পাওয়া যায়। প্রতিটি কমান্ডকে অবশ্যই ইন্টারফেস BlazeCommand প্রয়োগ করতে হবে এবং অবশ্যই @Command টীকা থাকতে হবে (এটি কিছুটা অ্যান্টিপ্যাটার্ন, এটি চমৎকার হবে যদি একটি কমান্ডের প্রয়োজনীয় সমস্ত মেটাডেটা BlazeCommand পদ্ধতি দ্বারা বর্ণনা করা হয়)

  3. কমান্ড লাইন অপশন পার্স করা হয়. প্রতিটি কমান্ডের বিভিন্ন কমান্ড লাইন বিকল্প রয়েছে, যা @কমান্ড @Command বর্ণনা করা হয়েছে।

  4. একটি ইভেন্ট বাস তৈরি করা হয়। ইভেন্ট বাসটি নির্মাণের সময় ঘটে যাওয়া ইভেন্টগুলির জন্য একটি প্রবাহ। এর মধ্যে কিছু বিল্ড ইভেন্ট প্রোটোকলের তত্ত্বাবধানে বাজেলের বাইরে রপ্তানি করা হয় যাতে বিল্ডটি কীভাবে হয় তা বিশ্বকে জানাতে।

  5. কমান্ড নিয়ন্ত্রণ পায়। সবচেয়ে আকর্ষণীয় কমান্ডগুলি হল যেগুলি একটি বিল্ড চালায়: বিল্ড, টেস্ট, রান, কভারেজ এবং আরও অনেক কিছু: এই কার্যকারিতাটি BuildTool দ্বারা প্রয়োগ করা হয়।

  6. কমান্ড লাইনে টার্গেট প্যাটার্নের সেট পার্স করা হয়েছে এবং ওয়াইল্ডকার্ড যেমন //pkg:all এবং //pkg/... সমাধান করা হয়েছে। এটি AnalysisPhaseRunner.evaluateTargetPatterns() এ প্রয়োগ করা হয়েছে এবং স্কাইফ্রেমে TargetPatternPhaseValue হিসাবে পুনরায় সংযোজন করা হয়েছে।

  7. লোডিং/বিশ্লেষণ পর্যায়টি অ্যাকশন গ্রাফ তৈরি করতে চালিত হয় (নির্দেশের জন্য একটি নির্দেশিত অ্যাসাইক্লিক গ্রাফ যা বিল্ডের জন্য কার্যকর করা প্রয়োজন)।

  8. মৃত্যুদন্ড পর্ব চালানো হয়. এর অর্থ হল অনুরোধ করা শীর্ষ-স্তরের লক্ষ্যগুলি তৈরি করার জন্য প্রয়োজনীয় প্রতিটি ক্রিয়া চালানো।

কমান্ড লাইন বিকল্প

একটি Bazel আহবানের জন্য কমান্ড লাইন বিকল্পগুলি একটি OptionsParsingResult অবজেক্টে বর্ণনা করা হয়েছে, যার ফলে বিকল্পগুলির মানগুলির জন্য "অপশন ক্লাস" থেকে একটি মানচিত্র রয়েছে। একটি "অপশন ক্লাস" হল OptionsBase এবং গ্রুপ কমান্ড লাইন অপশনগুলির একটি সাবক্লাস যা একে অপরের সাথে সম্পর্কিত। উদাহরণ স্বরূপ:

  1. একটি প্রোগ্রামিং ভাষার সাথে সম্পর্কিত বিকল্পগুলি ( CppOptions বা JavaOptions )। এগুলি FragmentOptions এর একটি সাবক্লাস হওয়া উচিত এবং অবশেষে একটি BuildOptions অবজেক্টে মোড়ানো হবে।
  2. Bazel যেভাবে ক্রিয়া সম্পাদন করে তার সাথে সম্পর্কিত বিকল্পগুলি ( ExecutionOptions )

এই বিকল্পগুলিকে বিশ্লেষণ পর্বে ব্যবহার করার জন্য ডিজাইন করা হয়েছে এবং (হয় জাভাতে RuleContext.getFragment() অথবা ctx.fragments এ ctx.fragments-এর মাধ্যমে)। তাদের মধ্যে কিছু (উদাহরণস্বরূপ, C++ স্ক্যানিং অন্তর্ভুক্ত করতে হবে কি না) এক্সিকিউশন পর্বে পড়া হয়, কিন্তু এর জন্য সর্বদা সুস্পষ্ট প্লাম্বিংয়ের প্রয়োজন হয় কারণ তখন BuildConfiguration উপলব্ধ নেই। আরও তথ্যের জন্য, "কনফিগারেশন" বিভাগটি দেখুন।

সতর্কতা: আমরা ভান করতে চাই যে OptionsBase দৃষ্টান্তগুলি অপরিবর্তনীয় এবং সেগুলিকে সেভাবে ব্যবহার করতে চাই (যেমন SkyKeys এর অংশ)। এটি এমন নয় এবং এগুলিকে সংশোধন করা সূক্ষ্ম উপায়ে ব্যাজেল ভাঙার একটি সত্যিই ভাল উপায় যা ডিবাগ করা কঠিন। দুর্ভাগ্যবশত, তাদের প্রকৃতপক্ষে অপরিবর্তনীয় করে তোলা একটি বড় প্রচেষ্টা। (অন্য কেউ এটির রেফারেন্স রাখার সুযোগ পাওয়ার আগে এবং hashCode() equals() বা হ্যাশকোড() কল করার আগে নির্মাণের পরপরই একটি FragmentOptions বিকল্পগুলি সংশোধন করা ঠিক আছে।)

Bazel নিম্নলিখিত উপায়ে বিকল্প ক্লাস সম্পর্কে শেখে:

  1. কিছু ব্যাজেল ( CommonCommandOptions ) এ হার্ড-ওয়ারযুক্ত
  2. প্রতিটি Bazel কমান্ডে @Command টীকা থেকে
  3. ConfiguredRuleClassProvider থেকে (এগুলি পৃথক প্রোগ্রামিং ভাষার সাথে সম্পর্কিত কমান্ড লাইন বিকল্প)
  4. Starlark নিয়মগুলি তাদের নিজস্ব বিকল্পগুলিও সংজ্ঞায়িত করতে পারে ( এখানে দেখুন)

প্রতিটি বিকল্প (স্টারলার্ক-সংজ্ঞায়িত বিকল্পগুলি ব্যতীত) হল একটি FragmentOptions সাবক্লাসের সদস্য ভেরিয়েবল যেখানে @Option টীকা রয়েছে, যা কিছু হেল্প টেক্সট সহ কমান্ড লাইন বিকল্পের নাম এবং প্রকার নির্দিষ্ট করে।

একটি কমান্ড লাইন বিকল্পের মান জাভা ধরনের সাধারণত কিছু সহজ (একটি স্ট্রিং, একটি পূর্ণসংখ্যা, একটি বুলিয়ান, একটি লেবেল, ইত্যাদি)। যাইহোক, আমরা আরও জটিল ধরণের বিকল্পগুলিকে সমর্থন করি; এই ক্ষেত্রে, কমান্ড লাইন স্ট্রিং থেকে ডেটা টাইপে রূপান্তর করার কাজটি com.google.devtools.common.options.Converter এর বাস্তবায়নে পড়ে।

উৎস গাছ, যেমন Bazel দ্বারা দেখা

Bazel সফ্টওয়্যার তৈরির ব্যবসায় রয়েছে, যা সোর্স কোড পড়ার এবং ব্যাখ্যা করার মাধ্যমে ঘটে। Bazel যে সোর্স কোডে কাজ করে তার সামগ্রিকতাকে "ওয়ার্কস্পেস" বলা হয় এবং এটিকে রিপোজিটরি, প্যাকেজ এবং নিয়মে গঠন করা হয়।

ভান্ডার

একটি "ভান্ডার" হল একটি উৎস গাছ যার উপর একজন বিকাশকারী কাজ করে; এটি সাধারণত একটি একক প্রকল্প প্রতিনিধিত্ব করে। ব্যাজেলের পূর্বপুরুষ, ব্লেজ, একটি মনোরেপোতে পরিচালনা করেছিলেন, অর্থাৎ, একটি একক উত্স গাছ যাতে বিল্ড চালানোর জন্য ব্যবহৃত সমস্ত উত্স কোড থাকে। ব্যাজেল, বিপরীতে, এমন প্রকল্পগুলিকে সমর্থন করে যার উত্স কোড একাধিক সংগ্রহস্থলে বিস্তৃত। যে রিপোজিটরি থেকে Bazel আমন্ত্রণ জানানো হয় তাকে "প্রধান সংগ্রহস্থল" বলা হয়, অন্যগুলিকে "বাহ্যিক সংগ্রহস্থল" বলা হয়।

একটি সংগ্রহস্থল তার রুট ডিরেক্টরিতে WORKSPACE (বা WORKSPACE.bazel ) নামক একটি ফাইল দ্বারা চিহ্নিত করা হয়। এই ফাইলটিতে এমন তথ্য রয়েছে যা সমগ্র বিল্ডের জন্য "গ্লোবাল", উদাহরণস্বরূপ, উপলব্ধ বহিরাগত সংগ্রহস্থলের সেট। এটি একটি নিয়মিত স্টারলার্ক ফাইলের মতো কাজ করে যার মানে হল যে কেউ অন্য স্টারলার্ক ফাইল load() করতে পারে। এটি সাধারণত রিপোজিটরিগুলিকে টেনে আনতে ব্যবহৃত হয় যা একটি রিপোজিটরির প্রয়োজন হয় যা স্পষ্টভাবে উল্লেখ করা হয় (আমরা এটিকে " deps.bzl প্যাটার্ন" বলি)

এক্সটার্নাল রিপোজিটরির কোড $OUTPUT_BASE/external এর অধীনে সিমলিংক বা ডাউনলোড করা হয়।

বিল্ড চালানোর সময়, পুরো উৎস গাছ একসঙ্গে টুকরা করা প্রয়োজন; এটি সিমলিংকফরেস্ট দ্বারা করা হয়েছে, যা মূল সংগ্রহস্থলের প্রতিটি প্যাকেজকে $EXECROOT এবং প্রতিটি external সংগ্রহস্থলকে $EXECROOT/external বা $EXECROOT/.. এর সাথে সিমলিং করে। ভান্ডার; এজন্যই আমরা এটি থেকে দূরে সরে যাচ্ছি)

প্যাকেজ

প্রতিটি সংগ্রহস্থল প্যাকেজ, সম্পর্কিত ফাইলের একটি সংগ্রহ এবং নির্ভরতাগুলির একটি স্পেসিফিকেশন নিয়ে গঠিত। এইগুলি BUILD বা BUILD.bazel নামক একটি ফাইল দ্বারা নির্দিষ্ট করা হয়। উভয়ই বিদ্যমান থাকলে, Bazel BUILD.bazel পছন্দ করে; BUILD ফাইলগুলি এখনও গৃহীত হওয়ার কারণ হল যে Bazel এর পূর্বপুরুষ, Blaze, এই ফাইলের নামটি ব্যবহার করেছেন। যাইহোক, এটি একটি সাধারণভাবে ব্যবহৃত পাথ সেগমেন্ট হিসাবে দেখা গেছে, বিশেষ করে উইন্ডোজে, যেখানে ফাইলের নামগুলি কেস-সংবেদনশীল।

প্যাকেজগুলি একে অপরের থেকে স্বাধীন: একটি প্যাকেজের BUILD ফাইলে পরিবর্তন অন্য প্যাকেজগুলিকে পরিবর্তন করতে পারে না। BUILD ফাইলের সংযোজন বা অপসারণ অন্যান্য প্যাকেজগুলিকে _পরিবর্তন করতে পারে, যেহেতু রিকার্সিভ গ্লোবগুলি প্যাকেজের সীমানায় থেমে যায় এবং এইভাবে একটি BUILD ফাইলের উপস্থিতি পুনরাবৃত্তি বন্ধ করে দেয়।

একটি BUILD ফাইলের মূল্যায়নকে "প্যাকেজ লোডিং" বলা হয়। এটি PackageFactory ক্লাসে প্রয়োগ করা হয়েছে, স্টারলার্ক ইন্টারপ্রেটারকে কল করে কাজ করে এবং উপলব্ধ নিয়ম ক্লাসের সেটের জ্ঞান প্রয়োজন। প্যাকেজ লোডিংয়ের ফলাফল হল একটি Package অবজেক্ট। এটি বেশিরভাগই একটি স্ট্রিং (একটি লক্ষ্যের নাম) থেকে লক্ষ্যমাত্রায় একটি মানচিত্র।

প্যাকেজ লোড করার সময় জটিলতার একটি বড় অংশ হল গ্লোবিং: Bazel-এর জন্য প্রতিটি সোর্স ফাইল স্পষ্টভাবে তালিকাভুক্ত করার প্রয়োজন নেই এবং পরিবর্তে গ্লোব (যেমন glob(["**/*.java"]) ) চালাতে পারে। শেল থেকে ভিন্ন, এটি রিকার্সিভ গ্লোব সমর্থন করে যা সাব-ডিরেক্টরিতে (কিন্তু সাবপ্যাকেজে নয়)। এটির জন্য ফাইল সিস্টেমে অ্যাক্সেসের প্রয়োজন এবং যেহেতু এটি ধীর হতে পারে, আমরা এটিকে সমান্তরালভাবে এবং যতটা সম্ভব দক্ষতার সাথে চালানোর জন্য সব ধরণের কৌশল প্রয়োগ করি।

গ্লোবিং নিম্নলিখিত ক্লাসে প্রয়োগ করা হয়:

  • LegacyGlobber , একটি দ্রুত এবং আনন্দের সাথে স্কাইফ্রেম-অজানা গ্লোবার
  • SkyframeHybridGlobber , একটি সংস্করণ যা স্কাইফ্রেম ব্যবহার করে এবং "স্কাইফ্রেম পুনরায় চালু হওয়া" এড়াতে লিগ্যাসি গ্লোবারে ফিরে যায় (নীচে বর্ণিত)

Package ক্লাসে এমন কিছু সদস্য রয়েছে যেগুলি একচেটিয়াভাবে WORKSPACE ফাইল পার্স করার জন্য ব্যবহৃত হয় এবং যা প্রকৃত প্যাকেজগুলির জন্য অর্থপূর্ণ নয়। এটি একটি ডিজাইনের ত্রুটি কারণ নিয়মিত প্যাকেজগুলি বর্ণনা করে এমন বস্তুগুলিতে এমন ক্ষেত্র থাকা উচিত নয় যা অন্য কিছু বর্ণনা করে। এর মধ্যে রয়েছে:

  • সংগ্রহস্থল ম্যাপিং
  • নিবন্ধিত টুলচেইন
  • নিবন্ধিত মৃত্যুদন্ড প্ল্যাটফর্ম

আদর্শভাবে, নিয়মিত প্যাকেজ পার্স করা থেকে WORKSPACE ফাইল পার্স করার মধ্যে আরও বিভাজন থাকবে যাতে Package উভয়ের প্রয়োজন মেটাতে না পারে। এটি করা দুর্ভাগ্যবশত কঠিন কারণ দুটি বেশ গভীরভাবে জড়িত।

লেবেল, লক্ষ্য এবং নিয়ম

প্যাকেজগুলি লক্ষ্যবস্তু নিয়ে গঠিত, যার নিম্নলিখিত প্রকার রয়েছে:

  1. ফাইল: যে জিনিসগুলি বিল্ডের ইনপুট বা আউটপুট। বাজেল ভাষায়, আমরা সেগুলোকে আর্টিফ্যাক্ট বলি (অন্য জায়গায় আলোচনা করা হয়েছে)। নির্মাণের সময় তৈরি করা সমস্ত ফাইল লক্ষ্য নয়; Bazel-এর একটি আউটপুটে সংশ্লিষ্ট লেবেল না থাকা সাধারণ।
  2. নিয়ম: এইগুলি এর ইনপুট থেকে এর আউটপুটগুলি বের করার পদক্ষেপগুলি বর্ণনা করে। এগুলি সাধারণত একটি প্রোগ্রামিং ভাষার সাথে যুক্ত থাকে (যেমন cc_library , java_library বা py_library ), তবে কিছু ভাষা-অজ্ঞেয়বাদী আছে (যেমন genrule বা filegroup )
  3. প্যাকেজ গ্রুপ: দৃশ্যমানতা বিভাগে আলোচনা করা হয়েছে।

একটি লক্ষ্যের নাম একটি লেবেল বলা হয়। লেবেলগুলির সিনট্যাক্স হল @repo//pac/kage:name , যেখানে repo হল সেই রিপোজিটরির নাম যেখানে লেবেলটি রয়েছে, pac/kage হল একটি ডিরেক্টরি যার BUILD ফাইল রয়েছে এবং name হল ফাইলের পাথ (যদি লেবেল একটি উৎস ফাইলকে বোঝায়) প্যাকেজের ডিরেক্টরির সাথে সম্পর্কিত। কমান্ড লাইনে একটি লক্ষ্য উল্লেখ করার সময়, লেবেলের কিছু অংশ বাদ দেওয়া যেতে পারে:

  1. রিপোজিটরিটি বাদ দেওয়া হলে, লেবেলটি মূল সংগ্রহস্থলে নেওয়া হয়।
  2. যদি প্যাকেজ অংশটি বাদ দেওয়া হয় (যেমন name বা :name ), লেবেলটি বর্তমান কার্যকারী ডিরেক্টরির প্যাকেজের মধ্যে নেওয়া হয় (উপলেভেল রেফারেন্স (..) সমন্বিত আপেক্ষিক পাথ অনুমোদিত নয়)

এক ধরনের নিয়ম (যেমন "C++ লাইব্রেরি") কে "রুল ক্লাস" বলা হয়। নিয়ম ক্লাসগুলি হয় স্টারলার্ক ( rule() ফাংশনে) বা জাভাতে (তথাকথিত "নেটিভ রুলস", টাইপ RuleClass ) এ প্রয়োগ করা যেতে পারে। দীর্ঘমেয়াদে, প্রতিটি ভাষা-নির্দিষ্ট নিয়ম স্টারলার্ক-এ প্রয়োগ করা হবে, তবে কিছু উত্তরাধিকার নিয়ম পরিবার (যেমন জাভা বা C++) এখনও জাভাতে রয়েছে।

load() স্টেটমেন্ট ব্যবহার করে BUILD ফাইলের শুরুতে Starlark রুল ক্লাস ইম্পোর্ট করতে হবে, যেখানে Java রুল ক্লাসগুলি ConfiguredRuleClassProvider এর সাথে নিবন্ধিত হওয়ার কারণে Bazel দ্বারা "সহজাতভাবে" পরিচিত।

নিয়ম ক্লাসে তথ্য থাকে যেমন:

  1. এর বৈশিষ্ট্যগুলি (যেমন srcs , deps ): তাদের প্রকার, ডিফল্ট মান, সীমাবদ্ধতা ইত্যাদি।
  2. কনফিগারেশন ট্রানজিশন এবং প্রতিটি অ্যাট্রিবিউটের সাথে সংযুক্ত দিক, যদি থাকে
  3. নিয়মের বাস্তবায়ন
  4. ট্রানজিটিভ তথ্য প্রদানকারীরা নিয়ম "সাধারণত" তৈরি করে

পরিভাষা নোট: কোড বেসে, আমরা প্রায়ই একটি নিয়ম শ্রেণীর দ্বারা তৈরি লক্ষ্য বোঝাতে "নিয়ম" ব্যবহার করি। কিন্তু স্টারলার্ক এবং ব্যবহারকারী-মুখী ডকুমেন্টেশনে, "নিয়ম" ব্যবহার করা উচিত একচেটিয়াভাবে নিয়ম শ্রেণীর নিজেই উল্লেখ করার জন্য; লক্ষ্য মাত্র একটি "লক্ষ্য"। এছাড়াও মনে রাখবেন যে RuleClass এর নামে "class" থাকা সত্ত্বেও, একটি নিয়ম ক্লাস এবং সেই ধরনের লক্ষ্যগুলির মধ্যে কোন জাভা উত্তরাধিকার সম্পর্ক নেই।

স্কাইফ্রেম

বেজেলের অন্তর্নিহিত মূল্যায়ন কাঠামোকে স্কাইফ্রেম বলা হয়। এর মডেলটি হল যে একটি বিল্ড করার সময় যা কিছু তৈরি করা দরকার তা একটি নির্দেশিত অ্যাসাইক্লিক গ্রাফে সংগঠিত হয় যার প্রান্তগুলি ডেটার যেকোন টুকরো থেকে তার নির্ভরতার দিকে নির্দেশ করে, অর্থাৎ, এটি তৈরি করার জন্য অন্যান্য ডেটার টুকরোগুলি জানা দরকার।

গ্রাফের SkyValue s বলা হয় এবং তাদের নামগুলিকে SkyKey s বলা হয়। উভয়ই গভীরভাবে অপরিবর্তনীয়; শুধুমাত্র অপরিবর্তনীয় বস্তুই তাদের কাছ থেকে পৌঁছানো উচিত। এই অপরিবর্তনীয়টি প্রায় সবসময়ই ধারণ করে, এবং যদি এটি না থাকে (যেমন স্বতন্ত্র বিকল্প ক্লাসের জন্য BuildOptions , যা BuildConfigurationValue এবং এর SkyKey এর সদস্য) আমরা তাদের পরিবর্তন না করার জন্য বা শুধুমাত্র এমন উপায়ে পরিবর্তন করার জন্য সত্যিই কঠোর চেষ্টা করি যা বাইরে থেকে দেখা যায় না। এটি থেকে এটি অনুসরণ করে যে স্কাইফ্রেমের মধ্যে গণনা করা সমস্ত কিছু (যেমন কনফিগার করা লক্ষ্যগুলি) অবশ্যই অপরিবর্তনীয় হতে হবে।

স্কাইফ্রেম গ্রাফ পর্যবেক্ষণ করার সবচেয়ে সুবিধাজনক উপায় হল bazel dump --skyframe=detailed , যা গ্রাফটিকে ডাম্প করে, প্রতি লাইনে একটি SkyValue । ছোট বিল্ডগুলির জন্য এটি করা ভাল, যেহেতু এটি বেশ বড় হতে পারে।

Skyframe com.google.devtools.build.skyframe প্যাকেজে থাকে। অনুরূপ-নামযুক্ত প্যাকেজ com.google.devtools.build.lib.skyframe এ Skyframe-এর উপরে Bazel বাস্তবায়ন রয়েছে। স্কাইফ্রেম সম্পর্কে আরও তথ্য এখানে উপলব্ধ।

একটি প্রদত্ত SkyKey কে একটি SkyValue তে মূল্যায়ন করার জন্য, Skyframe কীটির প্রকারের সাথে সঙ্গতিপূর্ণ SkyFunction আহ্বান করবে। ফাংশনের মূল্যায়নের সময়, এটি SkyFunction.Environment.getValue() এর বিভিন্ন ওভারলোডগুলিকে কল করে Skyframe থেকে অন্যান্য নির্ভরতার অনুরোধ করতে পারে৷ এটি স্কাইফ্রেমের অভ্যন্তরীণ গ্রাফে সেই নির্ভরতাগুলি নিবন্ধন করার পার্শ্ব-প্রতিক্রিয়া রয়েছে, যাতে স্কাইফ্রেম তার কোনও নির্ভরতা পরিবর্তিত হলে ফাংশনটি পুনরায় মূল্যায়ন করতে জানবে। অন্য কথায়, স্কাইফ্রেমের ক্যাশিং এবং ইনক্রিমেন্টাল কম্পিউটেশন কাজ করে SkyFunction এবং SkyValue s-এর গ্রানুলারিটিতে।

যখনই একটি SkyFunction অনুপলব্ধ একটি নির্ভরতা অনুরোধ করে, getValue() নাল ফিরে আসবে। ফাংশনটি তখন স্কাইফ্রেমে নিয়ন্ত্রণ ফিরিয়ে আনতে হবে নিজেই নাল রিটার্ন করে। কিছু পরে, Skyframe অনুপলব্ধ নির্ভরতা মূল্যায়ন করবে, তারপর শুরু থেকে ফাংশনটি পুনরায় চালু করবে — শুধুমাত্র এই সময় getValue() কলটি একটি নন-নাল ফলাফলের সাথে সফল হবে।

এর একটি ফলাফল হল যে রিস্টার্ট করার আগে SkyFunction এর ভিতরে সম্পাদিত যেকোনো গণনা অবশ্যই পুনরাবৃত্তি করতে হবে। কিন্তু এটি ক্যাশে করা SkyValues ​​নির্ভরতা মূল্যায়ন করার জন্য করা কাজ অন্তর্ভুক্ত করে না। অতএব, আমরা সাধারণত এই সমস্যার সমাধান করে কাজ করি:

  1. ব্যাচে নির্ভরতা ঘোষণা করা ( getValuesAndExceptions() ব্যবহার করে ) পুনরায় আরম্ভের সংখ্যা সীমিত করতে।
  2. একটি SkyValue আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা SkyFunction বদল করা। এটি কৌশলগতভাবে করা উচিত, কারণ এতে মেমরির ব্যবহার বাড়ানোর সম্ভাবনা রয়েছে।
  3. SkyFunction.Environment.getState() ব্যবহার করে, অথবা "Skyframe এর পিছনে" একটি অ্যাডহক স্ট্যাটিক ক্যাশে রাখা, রিস্টার্টের মধ্যে স্টেট স্টোর করা।

মৌলিকভাবে, আমাদের এই ধরনের সমাধানের প্রয়োজন কারণ আমাদের নিয়মিতভাবে কয়েক হাজার ইন-ফ্লাইট স্কাইফ্রেম নোড থাকে এবং জাভা হালকা ওজনের থ্রেড সমর্থন করে না।

স্টারলার্ক

Starlark হল ডোমেন-নির্দিষ্ট ভাষা যা লোকেরা ব্যাজেল কনফিগার এবং প্রসারিত করতে ব্যবহার করে। এটি পাইথনের একটি সীমাবদ্ধ উপসেট হিসাবে কল্পনা করা হয়েছে যেটির ধরন অনেক কম, নিয়ন্ত্রণ প্রবাহের উপর আরও বিধিনিষেধ এবং সবচেয়ে গুরুত্বপূর্ণভাবে, সমবর্তী পাঠ সক্ষম করার জন্য শক্তিশালী অপরিবর্তনীয়তার গ্যারান্টি রয়েছে। এটি টুরিং-সম্পূর্ণ নয়, যা কিছু (কিন্তু সব নয়) ব্যবহারকারীকে ভাষার মধ্যে সাধারণ প্রোগ্রামিং কাজগুলি সম্পন্ন করার চেষ্টা করতে নিরুৎসাহিত করে।

Starlark net.starlark.java প্যাকেজে প্রয়োগ করা হয়েছে। এটি এখানে একটি স্বাধীন গো বাস্তবায়নও রয়েছে । ব্যাজেলে ব্যবহৃত জাভা বাস্তবায়ন বর্তমানে একটি দোভাষী।

Starlark বিভিন্ন প্রসঙ্গে ব্যবহৃত হয়, যার মধ্যে রয়েছে:

  1. BUILD ভাষা। এখানেই নতুন নিয়ম সংজ্ঞায়িত করা হয়েছে। এই প্রসঙ্গে চলমান স্টারলার্ক কোডে শুধুমাত্র BUILD ফাইলের বিষয়বস্তু এবং এটি দ্বারা লোড করা .bzl ফাইলগুলিতে অ্যাক্সেস রয়েছে।
  2. নিয়মের সংজ্ঞা। এইভাবে নতুন নিয়ম (যেমন একটি নতুন ভাষার জন্য সমর্থন) সংজ্ঞায়িত করা হয়। এই প্রসঙ্গে চলমান স্টারলার্ক কোডের সরাসরি নির্ভরতা দ্বারা প্রদত্ত কনফিগারেশন এবং ডেটাতে অ্যাক্সেস রয়েছে (এর পরে আরও বেশি)।
  3. ওয়ার্কস্পেস ফাইল। এখানেই বাহ্যিক সংগ্রহস্থলগুলি (কোড যা মূল উত্স ট্রিতে নেই) সংজ্ঞায়িত করা হয়েছে।
  4. সংগ্রহস্থল নিয়ম সংজ্ঞা. এখানেই নতুন বাহ্যিক সংগ্রহস্থলের ধরন সংজ্ঞায়িত করা হয়েছে। এই প্রসঙ্গে চলমান স্টারলার্ক কোডটি মেশিনে নির্বিচারে কোড চালাতে পারে যেখানে ব্যাজেল চলছে এবং কর্মক্ষেত্রের বাইরে পৌঁছাতে পারে।

BUILD এবং .bzl ফাইলগুলির জন্য উপলব্ধ উপভাষাগুলি কিছুটা আলাদা কারণ তারা বিভিন্ন জিনিস প্রকাশ করে। পার্থক্যের একটি তালিকা এখানে উপলব্ধ।

Starlark সম্পর্কে আরও তথ্য এখানে উপলব্ধ।

লোডিং/বিশ্লেষণ পর্ব

লোডিং/বিশ্লেষণ পর্যায়টি হল যেখানে Bazel নির্ধারণ করে যে একটি নির্দিষ্ট নিয়ম তৈরি করতে কী কী ক্রিয়া প্রয়োজন। এর মৌলিক ইউনিট হল একটি "কনফিগার করা লক্ষ্য", যা বেশ সংবেদনশীলভাবে, একটি (লক্ষ্য, কনফিগারেশন) জোড়া।

এটিকে "লোডিং/বিশ্লেষণ পর্যায়" বলা হয় কারণ এটি দুটি স্বতন্ত্র অংশে বিভক্ত হতে পারে, যা সিরিয়ালাইজ করা হত, কিন্তু তারা এখন সময়ের সাথে ওভারল্যাপ করতে পারে:

  1. প্যাকেজ লোড হচ্ছে, অর্থাৎ, BUILD ফাইলগুলিকে Package অবজেক্টে পরিণত করা যা তাদের প্রতিনিধিত্ব করে
  2. কনফিগার করা লক্ষ্যগুলি বিশ্লেষণ করা, অর্থাৎ, অ্যাকশন গ্রাফ তৈরি করতে নিয়মের বাস্তবায়ন চালানো

কমান্ড লাইনে অনুরোধ করা কনফিগার করা লক্ষ্যগুলির ট্রানজিটিভ ক্লোজারে প্রতিটি কনফিগার করা লক্ষ্যকে নীচে-উপরে বিশ্লেষণ করতে হবে; অর্থাৎ, প্রথমে লিফ নোড, তারপর কমান্ড লাইনে থাকা পর্যন্ত। একটি একক কনফিগার করা লক্ষ্যের বিশ্লেষণের ইনপুটগুলি হল:

  1. কনফিগারেশন। ("কিভাবে" সেই নিয়মটি তৈরি করতে হয়; উদাহরণস্বরূপ, টার্গেট প্ল্যাটফর্ম কিন্তু কমান্ড লাইন বিকল্পগুলির মতো জিনিসগুলিও ব্যবহারকারী C++ কম্পাইলারের কাছে পাস করতে চায়)
  2. সরাসরি নির্ভরতা। তাদের ট্রানজিটিভ তথ্য প্রদানকারী নিয়ম বিশ্লেষণ করা হচ্ছে উপলব্ধ. এগুলিকে সেরকম বলা হয় কারণ তারা কনফিগার করা লক্ষ্যের ট্রানজিটিভ ক্লোজারে তথ্যের একটি "রোল-আপ" প্রদান করে, যেমন ক্লাসপাথের সমস্ত .jar ফাইল বা সমস্ত .o ফাইল যা একটি C++ এর সাথে লিঙ্ক করা দরকার। বাইনারি)
  3. টার্গেট নিজেই । এটি লক্ষ্যমাত্রার প্যাকেজ লোড করার ফলাফল। নিয়মের জন্য, এটি এর বৈশিষ্ট্যগুলি অন্তর্ভুক্ত করে, যা সাধারণত গুরুত্বপূর্ণ।
  4. কনফিগার করা লক্ষ্য বাস্তবায়ন। নিয়মের জন্য, এটি স্টারলার্ক বা জাভাতে হতে পারে। সমস্ত নন-রুল কনফিগার করা লক্ষ্যগুলি জাভাতে প্রয়োগ করা হয়।

একটি কনফিগার করা লক্ষ্য বিশ্লেষণের আউটপুট হল:

  1. ট্রানজিটিভ ইনফো প্রোভাইডার যেগুলি লক্ষ্যগুলি কনফিগার করেছে যা এটির উপর নির্ভর করে অ্যাক্সেস করতে পারে৷
  2. এটি যে শিল্পকর্মগুলি তৈরি করতে পারে এবং যে ক্রিয়াগুলি তাদের তৈরি করে।

জাভা নিয়মে দেওয়া API হল RuleContext , যা Starlark নিয়মের ctx আর্গুমেন্টের সমতুল্য। এর এপিআই আরও শক্তিশালী, কিন্তু একই সময়ে, খারাপ জিনিসগুলি করা সহজ, উদাহরণ স্বরূপ কোড লেখা যার সময় বা স্থান জটিলতা চতুর্মুখী (বা খারাপ), একটি জাভা ব্যতিক্রম সহ বাজেল সার্ভার ক্র্যাশ করা বা লঙ্ঘন করা invariants (যেমন অসাবধানতাবশত একটি Options উদাহরণ পরিবর্তন করে বা একটি কনফিগার করা লক্ষ্য পরিবর্তনযোগ্য করে)

যে অ্যালগরিদমটি একটি কনফিগার করা টার্গেটের সরাসরি নির্ভরতা নির্ধারণ করে তা DependencyResolver.dependentNodeMap() এ থাকে।

কনফিগারেশন

কনফিগারেশন হল একটি লক্ষ্য তৈরি করার "কিভাবে": কোন প্ল্যাটফর্মের জন্য, কোন কমান্ড লাইন বিকল্পগুলির সাথে, ইত্যাদি।

একই বিল্ডে একাধিক কনফিগারেশনের জন্য একই লক্ষ্য তৈরি করা যেতে পারে। এটি উপযোগী, উদাহরণস্বরূপ, যখন একই কোড ব্যবহার করা হয় একটি টুলের জন্য যা বিল্ড করার সময় এবং টার্গেট কোডের জন্য এবং আমরা ক্রস-কম্পাইল করছি বা যখন আমরা একটি ফ্যাট অ্যান্ড্রয়েড অ্যাপ তৈরি করছি (যেটিতে একাধিক CPU-এর জন্য নেটিভ কোড থাকে স্থাপত্য)

ধারণাগতভাবে, কনফিগারেশনটি একটি BuildOptions উদাহরণ। যাইহোক, অনুশীলনে, BuildOptions BuildConfiguration মোড়ানো হয় যা কার্যকারিতার অতিরিক্ত বিভিন্ন অংশ প্রদান করে। এটি নির্ভরতা গ্রাফের শীর্ষ থেকে নীচের দিকে প্রচার করে। যদি এটি পরিবর্তিত হয়, বিল্ডটি পুনরায় বিশ্লেষণ করা প্রয়োজন।

এর ফলে পুরো বিল্ডটিকে পুনরায় বিশ্লেষণ করতে হবে যেমন, উদাহরণস্বরূপ, অনুরোধ করা পরীক্ষা চালানোর সংখ্যা পরিবর্তিত হলে, যদিও এটি শুধুমাত্র পরীক্ষার লক্ষ্যগুলিকে প্রভাবিত করে (আমাদের কনফিগারেশনগুলি "ট্রিম" করার পরিকল্পনা রয়েছে যাতে এটি না হয়, কিন্তু এটি এখনও প্রস্তুত নয়)।

যখন একটি নিয়ম বাস্তবায়নের জন্য কনফিগারেশনের অংশের প্রয়োজন হয়, তখন এটিকে RuleClass.Builder.requiresConfigurationFragments() ব্যবহার করে তার সংজ্ঞায় এটি ঘোষণা করতে হবে। এটি উভয়ই ভুল এড়াতে (যেমন জাভা ফ্র্যাগমেন্ট ব্যবহার করে পাইথন নিয়ম) এবং কনফিগারেশন ট্রিমিং সহজতর করার জন্য যাতে পাইথন বিকল্পগুলি পরিবর্তন হলে, C++ লক্ষ্যগুলি পুনরায় বিশ্লেষণ করার প্রয়োজন না হয়।

একটি নিয়মের কনফিগারেশন অগত্যা তার "পিতামাতার" নিয়মের মতো নয়৷ নির্ভরতা প্রান্তে কনফিগারেশন পরিবর্তন করার প্রক্রিয়াটিকে "কনফিগারেশন ট্রানজিশন" বলা হয়। এটি দুটি জায়গায় ঘটতে পারে:

  1. নির্ভরতার প্রান্তে। এই রূপান্তরগুলি Attribute.Builder.cfg() এ নির্দিষ্ট করা হয়েছে এবং একটি Rule (যেখানে রূপান্তর ঘটে) এবং একটি BuildOptions (মূল কনফিগারেশন) থেকে এক বা একাধিক BuildOptions (আউটপুট কনফিগারেশন) এর ফাংশন।
  2. কনফিগার করা লক্ষ্যে যেকোন আগত প্রান্তে। এগুলি RuleClass.Builder.cfg() এ উল্লেখ করা হয়েছে।

প্রাসঙ্গিক ক্লাস হল TransitionFactory এবং ConfigurationTransition

কনফিগারেশন ট্রানজিশন ব্যবহার করা হয়, উদাহরণস্বরূপ:

  1. ঘোষণা করার জন্য যে একটি নির্দিষ্ট নির্ভরতা নির্মাণের সময় ব্যবহৃত হয় এবং এটি কার্যকরী আর্কিটেকচারে তৈরি করা উচিত
  2. একাধিক আর্কিটেকচারের জন্য (যেমন ফ্যাট অ্যান্ড্রয়েড APK-এ নেটিভ কোডের জন্য) একটি নির্দিষ্ট নির্ভরতা তৈরি করতে হবে তা ঘোষণা করতে

যদি একটি কনফিগারেশন ট্রানজিশন একাধিক কনফিগারেশনে পরিণত হয়, এটিকে একটি বিভক্ত রূপান্তর বলা হয়।

স্টারলার্কেও কনফিগারেশন ট্রানজিশন প্রয়োগ করা যেতে পারে ( এখানে ডকুমেন্টেশন)

পরিবর্তনশীল তথ্য প্রদানকারী

ট্রানজিটিভ ইনফো প্রদানকারীরা কনফিগার করা লক্ষ্যগুলির জন্য একটি উপায় (এবং _only _ওয়ে) যা এটির উপর নির্ভর করে এমন অন্যান্য কনফিগার করা লক্ষ্যগুলি সম্পর্কে কিছু বলার জন্য। তাদের নামে "ট্রানজিটিভ" হওয়ার কারণ হল এটি সাধারণত কনফিগার করা টার্গেটের ট্রানজিটিভ ক্লোজারের কিছু ধরণের রোল-আপ।

জাভা ট্রানজিটিভ ইনফো প্রোভাইডার এবং স্টারলার্কের মধ্যে সাধারণত 1:1 চিঠিপত্র থাকে (ব্যতিক্রমটি হল DefaultInfo যা FileProvider , FilesToRunProvider এবং RunfilesProvider এর সংমিশ্রণ কারণ সেই API-কে জাভা ট্রান্সলিটারেশনের একটি সরাসরি প্রতিবর্ণীকরণের চেয়ে বেশি Starlark-ish বলে মনে করা হয়েছিল। ) তাদের মূল নিম্নলিখিত জিনিসগুলির মধ্যে একটি:

  1. একটি জাভা ক্লাস অবজেক্ট। এটি শুধুমাত্র প্রদানকারীদের জন্য উপলব্ধ যারা Starlark থেকে অ্যাক্সেসযোগ্য নয়। এই প্রদানকারীরা TransitiveInfoProvider এর একটি সাবক্লাস।
  2. একটি স্ট্রিং। এটি উত্তরাধিকার এবং ব্যাপকভাবে নিরুৎসাহিত করা হয় কারণ এটি নামের সংঘর্ষের জন্য সংবেদনশীল। এই ধরনের ট্রানজিটিভ ইনফো প্রদানকারীরা হল build.lib.packages.Info এর সরাসরি সাবক্লাস।
  3. একটি প্রদানকারী প্রতীক। এটি provider() ফাংশন ব্যবহার করে Starlark থেকে তৈরি করা যেতে পারে এবং এটি নতুন প্রদানকারী তৈরি করার প্রস্তাবিত উপায়। জাভাতে একটি Provider.Key উদাহরণ দ্বারা প্রতীকটি উপস্থাপন করা হয়।

জাভাতে বাস্তবায়িত নতুন প্রদানকারীকে BuiltinProvider ব্যবহার করে প্রয়োগ করা উচিত। NativeProvider (এটি সরানোর জন্য আমাদের এখনও সময় নেই) এবং স্টারলার্ক থেকে TransitiveInfoProvider সাবক্লাসগুলি অ্যাক্সেস করা যাবে না।

লক্ষ্যমাত্রা কনফিগার করা হয়েছে

কনফিগার করা লক্ষ্যগুলি RuleConfiguredTargetFactory হিসাবে প্রয়োগ করা হয়। জাভাতে বাস্তবায়িত প্রতিটি নিয়ম শ্রেণীর জন্য একটি সাবক্লাস রয়েছে। Starlark কনফিগার করা লক্ষ্যগুলি StarlarkRuleConfiguredTargetUtil.buildRule() এর মাধ্যমে তৈরি করা হয়।

কনফিগার করা লক্ষ্য কারখানাগুলিকে তাদের রিটার্ন মান তৈরি করতে RuleConfiguredTargetBuilder ব্যবহার করা উচিত। এটি নিম্নলিখিত জিনিসগুলি নিয়ে গঠিত:

  1. তাদের filesToBuild , "এই নিয়মের প্রতিনিধিত্বকারী ফাইলগুলির সেট" এর অস্পষ্ট ধারণা। কনফিগার করা টার্গেট কমান্ড লাইনে বা একটি জেনরুলের srcs-এ থাকলে এই ফাইলগুলি তৈরি হয়।
  2. তাদের রানফাইল, নিয়মিত এবং ডেটা।
  3. তাদের আউটপুট গ্রুপ. এগুলি বিভিন্ন "ফাইলের অন্যান্য সেট" যা নিয়ম তৈরি করতে পারে। BUILD-এ ফাইলগ্রুপ নিয়মের output_group অ্যাট্রিবিউট ব্যবহার করে এবং জাভাতে OutputGroupInfo প্রদানকারী ব্যবহার করে সেগুলি অ্যাক্সেস করা যেতে পারে।

রানফাইলস

কিছু বাইনারি চালানোর জন্য ডেটা ফাইল প্রয়োজন। একটি বিশিষ্ট উদাহরণ হল পরীক্ষা যা ইনপুট ফাইলের প্রয়োজন। এটি Bazel-এ "রানফাইলস" ধারণা দ্বারা উপস্থাপিত হয়। একটি "রানফাইলস ট্রি" হল একটি নির্দিষ্ট বাইনারির জন্য ডেটা ফাইলগুলির একটি ডিরেক্টরি ট্রি। এটি ফাইল সিস্টেমে একটি সিমলিঙ্ক ট্রি হিসাবে তৈরি করা হয়েছে যার সাথে পৃথক সিমলিঙ্কগুলি আউটপুট ট্রিগুলির উত্সের ফাইলগুলির দিকে নির্দেশ করে।

রানফাইলগুলির একটি সেট একটি Runfiles উদাহরণ হিসাবে উপস্থাপন করা হয়। এটি ধারণাগতভাবে রানফাইলস ট্রিতে একটি ফাইলের পথ থেকে Artifact উদাহরণ পর্যন্ত একটি মানচিত্র যা এটিকে উপস্থাপন করে। এটি দুটি কারণে একটি একক Map চেয়ে একটু বেশি জটিল:

  • বেশিরভাগ সময়, একটি ফাইলের রানফাইল পাথ তার execpath হিসাবে একই হয়। আমরা কিছু RAM বাঁচাতে এটি ব্যবহার করি।
  • রানফাইল ট্রিগুলিতে বিভিন্ন লিগ্যাসি ধরণের এন্ট্রি রয়েছে, যেগুলিকেও উপস্থাপন করা দরকার।

রানফাইলগুলি RunfilesProvider ব্যবহার করে সংগ্রহ করা হয়: এই ক্লাসের একটি উদাহরণ রানফাইলগুলিকে একটি কনফিগার করা লক্ষ্য (যেমন একটি লাইব্রেরি) এবং এর ট্রানজিটিভ ক্লোজার চাহিদাগুলিকে উপস্থাপন করে এবং সেগুলি একটি নেস্টেড সেটের মতো সংগ্রহ করা হয় (আসলে, এগুলি কভারের নীচে নেস্টেড সেট ব্যবহার করে প্রয়োগ করা হয়) : প্রতিটি টার্গেট তার নির্ভরতার রানফাইলগুলিকে একত্রিত করে, তার নিজস্ব কিছু যোগ করে, তারপর নির্ভরতা গ্রাফে ফলাফলের সেটটিকে উপরের দিকে পাঠায়। একটি RunfilesProvider দৃষ্টান্তে দুটি Runfiles দৃষ্টান্ত রয়েছে, একটি যখন নিয়মটি "ডেটা" বৈশিষ্ট্যের মাধ্যমে নির্ভর করে এবং একটি অন্য প্রতিটি ধরণের ইনকামিং নির্ভরতার জন্য। এর কারণ হল একটি টার্গেট কখনও কখনও ভিন্ন রানফাইল উপস্থাপন করে যখন অন্যথায় ডেটা অ্যাট্রিবিউটের উপর নির্ভর করে। এটি একটি অবাঞ্ছিত উত্তরাধিকার আচরণ যা আমরা এখনও অপসারণ করতে পারিনি।

বাইনারিগুলির রানফাইলগুলিকে RunfilesSupport এর উদাহরণ হিসাবে উপস্থাপন করা হয়। এটি Runfiles থেকে আলাদা কারণ RunfilesSupport এর আসলে তৈরি হওয়ার ক্ষমতা রয়েছে ( Runfiles এর বিপরীতে, যা শুধুমাত্র একটি ম্যাপিং)। এটি নিম্নলিখিত অতিরিক্ত উপাদানগুলির প্রয়োজন:

  • ইনপুট রানফাইল ম্যানিফেস্ট। এটি রানফাইলস ট্রির একটি ধারাবাহিক বর্ণনা। এটি রানফাইলস ট্রির বিষয়বস্তুর জন্য একটি প্রক্সি হিসাবে ব্যবহৃত হয় এবং ব্যাজেল অনুমান করে যে ম্যানিফেস্টের বিষয়বস্তু পরিবর্তন হলেই রানফাইল ট্রি পরিবর্তিত হয়।
  • আউটপুট রানফাইল ম্যানিফেস্ট। এটি রানটাইম লাইব্রেরি দ্বারা ব্যবহৃত হয় যা রানফাইল ট্রি পরিচালনা করে, বিশেষ করে উইন্ডোজে, যা কখনও কখনও প্রতীকী লিঙ্ক সমর্থন করে না।
  • রানফাইলস মিডলম্যান। একটি রানফাইলস ট্রি বিদ্যমান থাকার জন্য, একজনকে সিমলিঙ্ক ট্রি এবং সিমলিংকগুলি নির্দেশিত শিল্পকর্ম তৈরি করতে হবে। In order to decrease the number of dependency edges, the runfiles middleman can be used to represent all these.
  • Command line arguments for running the binary whose runfiles the RunfilesSupport object represents.

Aspects

Aspects are a way to "propagate computation down the dependency graph". They are described for users of Bazel here . A good motivating example is protocol buffers: a proto_library rule should not know about any particular language, but building the implementation of a protocol buffer message (the “basic unit” of protocol buffers) in any programming language should be coupled to the proto_library rule so that if two targets in the same language depend on the same protocol buffer, it gets built only once.

Just like configured targets, they are represented in Skyframe as a SkyValue and the way they are constructed is very similar to how configured targets are built: they have a factory class called ConfiguredAspectFactory that has access to a RuleContext , but unlike configured target factories, it also knows about the configured target it is attached to and its providers.

The set of aspects propagated down the dependency graph is specified for each attribute using the Attribute.Builder.aspects() function. There are a few confusingly-named classes that participate in the process:

  1. AspectClass is the implementation of the aspect. It can be either in Java (in which case it's a subclass) or in Starlark (in which case it's an instance of StarlarkAspectClass ). It's analogous to RuleConfiguredTargetFactory .
  2. AspectDefinition is the definition of the aspect; it includes the providers it requires, the providers it provides and contains a reference to its implementation, such as the appropriate AspectClass instance. It's analogous to RuleClass .
  3. AspectParameters is a way to parametrize an aspect that is propagated down the dependency graph. It's currently a string to string map. A good example of why it's useful is protocol buffers: if a language has multiple APIs, the information as to which API the protocol buffers should be built for should be propagated down the dependency graph.
  4. Aspect represents all the data that's needed to compute an aspect that propagates down the dependency graph. It consists of the aspect class, its definition and its parameters.
  5. RuleAspect is the function that determines which aspects a particular rule should propagate. It's a Rule -> Aspect function.

A somewhat unexpected complication is that aspects can attach to other aspects; for example, an aspect collecting the classpath for a Java IDE will probably want to know about all the .jar files on the classpath, but some of them are protocol buffers. In that case, the IDE aspect will want to attach to the ( proto_library rule + Java proto aspect) pair.

The complexity of aspects on aspects is captured in the class AspectCollection .

Platforms and toolchains

Bazel supports multi-platform builds, that is, builds where there may be multiple architectures where build actions run and multiple architectures for which code is built. These architectures are referred to as platforms in Bazel parlance (full documentation here )

A platform is described by a key-value mapping from constraint settings (such as the concept of "CPU architecture") to constraint values (such as a particular CPU like x86_64). We have a "dictionary" of the most commonly used constraint settings and values in the @platforms repository.

The concept of toolchain comes from the fact that depending on what platforms the build is running on and what platforms are targeted, one may need to use different compilers; for example, a particular C++ toolchain may run on a specific OS and be able to target some other OSes. Bazel must determine the C++ compiler that is used based on the set execution and target platform (documentation for toolchains here ).

In order to do this, toolchains are annotated with the set of execution and target platform constraints they support. In order to do this, the definition of a toolchain are split into two parts:

  1. A toolchain() rule that describes the set of execution and target constraints a toolchain supports and tells what kind (such as C++ or Java) of toolchain it is (the latter is represented by the toolchain_type() rule)
  2. A language-specific rule that describes the actual toolchain (such as cc_toolchain() )

This is done in this way because we need to know the constraints for every toolchain in order to do toolchain resolution and language-specific *_toolchain() rules contain much more information than that, so they take more time to load.

Execution platforms are specified in one of the following ways:

  1. In the WORKSPACE file using the register_execution_platforms() function
  2. On the command line using the --extra_execution_platforms command line option

The set of available execution platforms is computed in RegisteredExecutionPlatformsFunction .

The target platform for a configured target is determined by PlatformOptions.computeTargetPlatform() . It's a list of platforms because we eventually want to support multiple target platforms, but it's not implemented yet.

The set of toolchains to be used for a configured target is determined by ToolchainResolutionFunction . It is a function of:

  • The set of registered toolchains (in the WORKSPACE file and the configuration)
  • The desired execution and target platforms (in the configuration)
  • The set of toolchain types that are required by the configured target (in UnloadedToolchainContextKey)
  • The set of execution platform constraints of the configured target (the exec_compatible_with attribute) and the configuration ( --experimental_add_exec_constraints_to_targets ), in UnloadedToolchainContextKey

Its result is an UnloadedToolchainContext , which is essentially a map from toolchain type (represented as a ToolchainTypeInfo instance) to the label of the selected toolchain. It's called "unloaded" because it does not contain the toolchains themselves, only their labels.

Then the toolchains are actually loaded using ResolvedToolchainContext.load() and used by the implementation of the configured target that requested them.

We also have a legacy system that relies on there being one single "host" configuration and target configurations being represented by various configuration flags, such as --cpu . We are gradually transitioning to the above system. In order to handle cases where people rely on the legacy configuration values, we have implemented platform mappings to translate between the legacy flags and the new-style platform constraints. Their code is in PlatformMappingFunction and uses a non-Starlark "little language".

Constraints

Sometimes one wants to designate a target as being compatible with only a few platforms. Bazel has (unfortunately) multiple mechanisms to achieve this end:

  • Rule-specific constraints
  • environment_group() / environment()
  • Platform constraints

Rule-specific constraints are mostly used within Google for Java rules; they are on their way out and they are not available in Bazel, but the source code may contain references to it. The attribute that governs this is called constraints= .

environment_group() and environment()

These rules are a legacy mechanism and are not widely used.

All build rules can declare which "environments" they can be built for, where a "environment" is an instance of the environment() rule.

There are various ways supported environments can be specified for a rule:

  1. Through the restricted_to= attribute. This is the most direct form of specification; it declares the exact set of environments the rule supports for this group.
  2. Through the compatible_with= attribute. This declares environments a rule supports in addition to "standard" environments that are supported by default.
  3. Through the package-level attributes default_restricted_to= and default_compatible_with= .
  4. Through default specifications in environment_group() rules. Every environment belongs to a group of thematically related peers (such as "CPU architectures", "JDK versions" or "mobile operating systems"). The definition of an environment group includes which of these environments should be supported by "default" if not otherwise specified by the restricted_to= / environment() attributes. A rule with no such attributes inherits all defaults.
  5. Through a rule class default. This overrides global defaults for all instances of the given rule class. This can be used, for example, to make all *_test rules testable without each instance having to explicitly declare this capability.

environment() is implemented as a regular rule whereas environment_group() is both a subclass of Target but not Rule ( EnvironmentGroup ) and a function that is available by default from Starlark ( StarlarkLibrary.environmentGroup() ) which eventually creates an eponymous target. This is to avoid a cyclic dependency that would arise because each environment needs to declare the environment group it belongs to and each environment group needs to declare its default environments.

A build can be restricted to a certain environment with the --target_environment command line option.

The implementation of the constraint check is in RuleContextConstraintSemantics and TopLevelConstraintSemantics .

Platform constraints

The current "official" way to describe what platforms a target is compatible with is by using the same constraints used to describe toolchains and platforms. It's under review in pull request #10945 .

Visibility

If you work on a large codebase with a lot of developers (like at Google), you want to take care to prevent everyone else from arbitrarily depending on your code. Otherwise, as per Hyrum's law , people will come to rely on behaviors that you considered to be implementation details.

Bazel supports this by the mechanism called visibility : you can declare that a particular target can only be depended on using the visibility attribute. This attribute is a little special because, although it holds a list of labels, these labels may encode a pattern over package names rather than a pointer to any particular target. (Yes, this is a design flaw.)

This is implemented in the following places:

  • The RuleVisibility interface represents a visibility declaration. It can be either a constant (fully public or fully private) or a list of labels.
  • Labels can refer to either package groups (predefined list of packages), to packages directly ( //pkg:__pkg__ ) or subtrees of packages ( //pkg:__subpackages__ ). This is different from the command line syntax, which uses //pkg:* or //pkg/... .
  • Package groups are implemented as their own target ( PackageGroup ) and configured target ( PackageGroupConfiguredTarget ). We could probably replace these with simple rules if we wanted to. Their logic is implemented with the help of: PackageSpecification , which corresponds to a single pattern like //pkg/... ; PackageGroupContents , which corresponds to a single package_group 's packages attribute; and PackageSpecificationProvider , which aggregates over a package_group and its transitive includes .
  • The conversion from visibility label lists to dependencies is done in DependencyResolver.visitTargetVisibility and a few other miscellaneous places.
  • The actual check is done in CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()

Nested sets

Oftentimes, a configured target aggregates a set of files from its dependencies, adds its own, and wraps the aggregate set into a transitive info provider so that configured targets that depend on it can do the same. Examples:

  • The C++ header files used for a build
  • The object files that represent the transitive closure of a cc_library
  • The set of .jar files that need to be on the classpath for a Java rule to compile or run
  • The set of Python files in the transitive closure of a Python rule

If we did this the naive way by using, for example, List or Set , we'd end up with quadratic memory usage: if there is a chain of N rules and each rule adds a file, we'd have 1+2+...+N collection members.

In order to get around this problem, we came up with the concept of a NestedSet . It's a data structure that is composed of other NestedSet instances and some members of its own, thereby forming a directed acyclic graph of sets. They are immutable and their members can be iterated over. We define multiple iteration order ( NestedSet.Order ): preorder, postorder, topological (a node always comes after its ancestors) and "don't care, but it should be the same each time".

The same data structure is called depset in Starlark.

Artifacts and Actions

The actual build consists of a set of commands that need to be run to produce the output the user wants. The commands are represented as instances of the class Action and the files are represented as instances of the class Artifact . They are arranged in a bipartite, directed, acyclic graph called the "action graph".

Artifacts come in two kinds: source artifacts (ones that are available before Bazel starts executing) and derived artifacts (ones that need to be built). Derived artifacts can themselves be multiple kinds:

  1. **Regular artifacts. **These are checked for up-to-dateness by computing their checksum, with mtime as a shortcut; we don't checksum the file if its ctime hasn't changed.
  2. Unresolved symlink artifacts. These are checked for up-to-dateness by calling readlink(). Unlike regular artifacts, these can be dangling symlinks. Usually used in cases where one then packs up some files into an archive of some sort.
  3. Tree artifacts. These are not single files, but directory trees. They are checked for up-to-dateness by checking the set of files in it and their contents. They are represented as a TreeArtifact .
  4. Constant metadata artifacts. Changes to these artifacts don't trigger a rebuild. This is used exclusively for build stamp information: we don't want to do a rebuild just because the current time changed.

There is no fundamental reason why source artifacts cannot be tree artifacts or unresolved symlink artifacts, it's just that we haven't implemented it yet (we should, though -- referencing a source directory in a BUILD file is one of the few known long-standing incorrectness issues with Bazel; we have an implementation that kind of works which is enabled by the BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM property)

A notable kind of Artifact are middlemen. They are indicated by Artifact instances that are the outputs of MiddlemanAction . They are used to special-case some things:

  • Aggregating middlemen are used to group artifacts together. This is so that if a lot of actions use the same large set of inputs, we don't have N*M dependency edges, only N+M (they are being replaced with nested sets)
  • Scheduling dependency middlemen ensure that an action runs before another. They are mostly used for linting but also for C++ compilation (see CcCompilationContext.createMiddleman() for an explanation)
  • Runfiles middlemen are used to ensure the presence of a runfiles tree so that one does not separately need to depend on the output manifest and every single artifact referenced by the runfiles tree.

Actions are best understood as a command that needs to be run, the environment it needs and the set of outputs it produces. The following things are the main components of the description of an action:

  • The command line that needs to be run
  • The input artifacts it needs
  • The environment variables that need to be set
  • Annotations that describe the environment (such as platform) it needs to run in \

There are also a few other special cases, like writing a file whose content is known to Bazel. They are a subclass of AbstractAction . Most of the actions are a SpawnAction or a StarlarkAction (the same, they should arguably not be separate classes), although Java and C++ have their own action types ( JavaCompileAction , CppCompileAction and CppLinkAction ).

We eventually want to move everything to SpawnAction ; JavaCompileAction is pretty close, but C++ is a bit of a special-case due to .d file parsing and include scanning.

The action graph is mostly "embedded" into the Skyframe graph: conceptually, the execution of an action is represented as an invocation of ActionExecutionFunction . The mapping from an action graph dependency edge to a Skyframe dependency edge is described in ActionExecutionFunction.getInputDeps() and Artifact.key() and has a few optimizations in order to keep the number of Skyframe edges low:

  • Derived artifacts do not have their own SkyValue s. Instead, Artifact.getGeneratingActionKey() is used to find out the key for the action that generates it
  • Nested sets have their own Skyframe key.

Shared actions

Some actions are generated by multiple configured targets; Starlark rules are more limited since they are only allowed to put their derived actions into a directory determined by their configuration and their package (but even so, rules in the same package can conflict), but rules implemented in Java can put derived artifacts anywhere.

This is considered to be a misfeature, but getting rid of it is really hard because it produces significant savings in execution time when, for example, a source file needs to be processed somehow and that file is referenced by multiple rules (handwave-handwave). This comes at the cost of some RAM: each instance of a shared action needs to be stored in memory separately.

If two actions generate the same output file, they must be exactly the same: have the same inputs, the same outputs and run the same command line. This equivalence relation is implemented in Actions.canBeShared() and it is verified between the analysis and execution phases by looking at every Action. This is implemented in SkyframeActionExecutor.findAndStoreArtifactConflicts() and is one of the few places in Bazel that requires a "global" view of the build.

The execution phase

This is when Bazel actually starts running build actions, such as commands that produce outputs.

The first thing Bazel does after the analysis phase is to determine what Artifacts need to be built. The logic for this is encoded in TopLevelArtifactHelper ; roughly speaking, it's the filesToBuild of the configured targets on the command line and the contents of a special output group for the explicit purpose of expressing "if this target is on the command line, build these artifacts".

The next step is creating the execution root. Since Bazel has the option to read source packages from different locations in the file system ( --package_path ), it needs to provide locally executed actions with a full source tree. This is handled by the class SymlinkForest and works by taking note of every target used in the analysis phase and building up a single directory tree that symlinks every package with a used target from its actual location. An alternative would be to pass the correct paths to commands (taking --package_path into account). This is undesirable because:

  • It changes action command lines when a package is moved from a package path entry to another (used to be a common occurrence)
  • It results in different command lines if an action is run remotely than if it's run locally
  • It requires a command line transformation specific to the tool in use (consider the difference between such as Java classpaths and C++ include paths)
  • Changing the command line of an action invalidates its action cache entry
  • --package_path is slowly and steadily being deprecated

Then, Bazel starts traversing the action graph (the bipartite, directed graph composed of actions and their input and output artifacts) and running actions. The execution of each action is represented by an instance of the SkyValue class ActionExecutionValue .

Since running an action is expensive, we have a few layers of caching that can be hit behind Skyframe:

  • ActionExecutionFunction.stateMap contains data to make Skyframe restarts of ActionExecutionFunction cheap
  • The local action cache contains data about the state of the file system
  • Remote execution systems usually also contain their own cache

The local action cache

This cache is another layer that sits behind Skyframe; even if an action is re-executed in Skyframe, it can still be a hit in the local action cache. It represents the state of the local file system and it's serialized to disk which means that when one starts up a new Bazel server, one can get local action cache hits even though the Skyframe graph is empty.

This cache is checked for hits using the method ActionCacheChecker.getTokenIfNeedToExecute() .

Contrary to its name, it's a map from the path of a derived artifact to the action that emitted it. The action is described as:

  1. The set of its input and output files and their checksum
  2. Its "action key", which is usually the command line that was executed, but in general, represents everything that's not captured by the checksum of the input files (such as for FileWriteAction , it's the checksum of the data that's written)

There is also a highly experimental “top-down action cache” that is still under development, which uses transitive hashes to avoid going to the cache as many times.

Input discovery and input pruning

Some actions are more complicated than just having a set of inputs. Changes to the set of inputs of an action come in two forms:

  • An action may discover new inputs before its execution or decide that some of its inputs are not actually necessary. The canonical example is C++, where it's better to make an educated guess about what header files a C++ file uses from its transitive closure so that we don't heed to send every file to remote executors; therefore, we have an option not to register every header file as an "input", but scan the source file for transitively included headers and only mark those header files as inputs that are mentioned in #include statements (we overestimate so that we don't need to implement a full C preprocessor) This option is currently hard-wired to "false" in Bazel and is only used at Google.
  • An action may realize that some files were not used during its execution. In C++, this is called ".d files": the compiler tells which header files were used after the fact, and in order to avoid the embarrassment of having worse incrementality than Make, Bazel makes use of this fact. This offers a better estimate than the include scanner because it relies on the compiler.

These are implemented using methods on Action:

  1. Action.discoverInputs() is called. It should return a nested set of Artifacts that are determined to be required. These must be source artifacts so that there are no dependency edges in the action graph that don't have an equivalent in the configured target graph.
  2. The action is executed by calling Action.execute() .
  3. At the end of Action.execute() , the action can call Action.updateInputs() to tell Bazel that not all of its inputs were needed. This can result in incorrect incremental builds if a used input is reported as unused.

When an action cache returns a hit on a fresh Action instance (such as created after a server restart), Bazel calls updateInputs() itself so that the set of inputs reflects the result of input discovery and pruning done before.

Starlark actions can make use of the facility to declare some inputs as unused using the unused_inputs_list= argument of ctx.actions.run() .

Various ways to run actions: Strategies/ActionContexts

Some actions can be run in different ways. For example, a command line can be executed locally, locally but in various kinds of sandboxes, or remotely. The concept that embodies this is called an ActionContext (or Strategy , since we successfully went only halfway with a rename...)

The life cycle of an action context is as follows:

  1. When the execution phase is started, BlazeModule instances are asked what action contexts they have. This happens in the constructor of ExecutionTool . Action context types are identified by a Java Class instance that refers to a sub-interface of ActionContext and which interface the action context must implement.
  2. The appropriate action context is selected from the available ones and is forwarded to ActionExecutionContext and BlazeExecutor .
  3. Actions request contexts using ActionExecutionContext.getContext() and BlazeExecutor.getStrategy() (there should really be only one way to do it…)

Strategies are free to call other strategies to do their jobs; this is used, for example, in the dynamic strategy that starts actions both locally and remotely, then uses whichever finishes first.

One notable strategy is the one that implements persistent worker processes ( WorkerSpawnStrategy ). The idea is that some tools have a long startup time and should therefore be reused between actions instead of starting one anew for every action (This does represent a potential correctness issue, since Bazel relies on the promise of the worker process that it doesn't carry observable state between individual requests)

If the tool changes, the worker process needs to be restarted. Whether a worker can be reused is determined by computing a checksum for the tool used using WorkerFilesHash . It relies on knowing which inputs of the action represent part of the tool and which represent inputs; this is determined by the creator of the Action: Spawn.getToolFiles() and the runfiles of the Spawn are counted as parts of the tool.

More information about strategies (or action contexts!):

  • Information about various strategies for running actions is available here .
  • Information about the dynamic strategy, one where we run an action both locally and remotely to see whichever finishes first is available here .
  • Information about the intricacies of executing actions locally is available here .

The local resource manager

Bazel can run many actions in parallel. The number of local actions that should be run in parallel differs from action to action: the more resources an action requires, the less instances should be running at the same time to avoid overloading the local machine.

This is implemented in the class ResourceManager : each action has to be annotated with an estimate of the local resources it requires in the form of a ResourceSet instance (CPU and RAM). Then when action contexts do something that requires local resources, they call ResourceManager.acquireResources() and are blocked until the required resources are available.

A more detailed description of local resource management is available here .

The structure of the output directory

Each action requires a separate place in the output directory where it places its outputs. The location of derived artifacts is usually as follows:

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

How is the name of the directory that is associated with a particular configuration determined? There are two conflicting desirable properties:

  1. If two configurations can occur in the same build, they should have different directories so that both can have their own version of the same action; otherwise, if the two configurations disagree about such as the command line of an action producing the same output file, Bazel doesn't know which action to choose (an "action conflict")
  2. If two configurations represent "roughly" the same thing, they should have the same name so that actions executed in one can be reused for the other if the command lines match: for example, changes to the command line options to the Java compiler should not result in C++ compile actions being re-run.

So far, we have not come up with a principled way of solving this problem, which has similarities to the problem of configuration trimming. A longer discussion of options is available here . The main problematic areas are Starlark rules (whose authors usually aren't intimately familiar with Bazel) and aspects, which add another dimension to the space of things that can produce the "same" output file.

The current approach is that the path segment for the configuration is <CPU>-<compilation mode> with various suffixes added so that configuration transitions implemented in Java don't result in action conflicts. In addition, a checksum of the set of Starlark configuration transitions is added so that users can't cause action conflicts. It is far from perfect. This is implemented in OutputDirectories.buildMnemonic() and relies on each configuration fragment adding its own part to the name of the output directory.

Tests

Bazel has rich support for running tests. It supports:

  • Running tests remotely (if a remote execution backend is available)
  • Running tests multiple times in parallel (for deflaking or gathering timing data)
  • Sharding tests (splitting test cases in same test over multiple processes for speed)
  • Re-running flaky tests
  • Grouping tests into test suites

Tests are regular configured targets that have a TestProvider, which describes how the test should be run:

  • The artifacts whose building result in the test being run. This is a "cache status" file that contains a serialized TestResultData message
  • The number of times the test should be run
  • The number of shards the test should be split into
  • Some parameters about how the test should be run (such as the test timeout)

Determining which tests to run

Determining which tests are run is an elaborate process.

First, during target pattern parsing, test suites are recursively expanded. The expansion is implemented in TestsForTargetPatternFunction . A somewhat surprising wrinkle is that if a test suite declares no tests, it refers to every test in its package. This is implemented in Package.beforeBuild() by adding an implicit attribute called $implicit_tests to test suite rules.

Then, tests are filtered for size, tags, timeout and language according to the command line options. This is implemented in TestFilter and is called from TargetPatternPhaseFunction.determineTests() during target parsing and the result is put into TargetPatternPhaseValue.getTestsToRunLabels() . The reason why rule attributes which can be filtered for are not configurable is that this happens before the analysis phase, therefore, the configuration is not available.

This is then processed further in BuildView.createResult() : targets whose analysis failed are filtered out and tests are split into exclusive and non-exclusive tests. It's then put into AnalysisResult , which is how ExecutionTool knows which tests to run.

In order to lend some transparency to this elaborate process, the tests() query operator (implemented in TestsFunction ) is available to tell which tests are run when a particular target is specified on the command line. It's unfortunately a reimplementation, so it probably deviates from the above in multiple subtle ways.

Running tests

The way the tests are run is by requesting cache status artifacts. This then results in the execution of a TestRunnerAction , which eventually calls the TestActionContext chosen by the --test_strategy command line option that runs the test in the requested way.

Tests are run according to an elaborate protocol that uses environment variables to tell tests what's expected from them. A detailed description of what Bazel expects from tests and what tests can expect from Bazel is available here . At the simplest, an exit code of 0 means success, anything else means failure.

In addition to the cache status file, each test process emits a number of other files. They are put in the "test log directory" which is the subdirectory called testlogs of the output directory of the target configuration:

  • test.xml , a JUnit-style XML file detailing the individual test cases in the test shard
  • test.log , the console output of the test. stdout and stderr are not separated.
  • test.outputs , the "undeclared outputs directory"; this is used by tests that want to output files in addition to what they print to the terminal.

There are two things that can happen during test execution that cannot during building regular targets: exclusive test execution and output streaming.

Some tests need to be executed in exclusive mode, for example not in parallel with other tests. This can be elicited either by adding tags=["exclusive"] to the test rule or running the test with --test_strategy=exclusive . Each exclusive test is run by a separate Skyframe invocation requesting the execution of the test after the "main" build. This is implemented in SkyframeExecutor.runExclusiveTest() .

Unlike regular actions, whose terminal output is dumped when the action finishes, the user can request the output of tests to be streamed so that they get informed about the progress of a long-running test. This is specified by the --test_output=streamed command line option and implies exclusive test execution so that outputs of different tests are not interspersed.

This is implemented in the aptly-named StreamedTestOutput class and works by polling changes to the test.log file of the test in question and dumping new bytes to the terminal where Bazel rules.

Results of the executed tests are available on the event bus by observing various events (such as TestAttempt , TestResult or TestingCompleteEvent ). They are dumped to the Build Event Protocol and they are emitted to the console by AggregatingTestListener .

Coverage collection

Coverage is reported by the tests in LCOV format in the files bazel-testlogs/$PACKAGE/$TARGET/coverage.dat .

To collect coverage, each test execution is wrapped in a script called collect_coverage.sh .

This script sets up the environment of the test to enable coverage collection and determine where the coverage files are written by the coverage runtime(s). It then runs the test. A test may itself run multiple subprocesses and consist of parts written in multiple different programming languages (with separate coverage collection runtimes). The wrapper script is responsible for converting the resulting files to LCOV format if necessary, and merges them into a single file.

The interposition of collect_coverage.sh is done by the test strategies and requires collect_coverage.sh to be on the inputs of the test. This is accomplished by the implicit attribute :coverage_support which is resolved to the value of the configuration flag --coverage_support (see TestConfiguration.TestOptions.coverageSupport )

Some languages do offline instrumentation, meaning that the coverage instrumentation is added at compile time (such as C++) and others do online instrumentation, meaning that coverage instrumentation is added at execution time.

Another core concept is baseline coverage . This is the coverage of a library, binary, or test if no code in it was run. The problem it solves is that if you want to compute the test coverage for a binary, it is not enough to merge the coverage of all of the tests because there may be code in the binary that is not linked into any test. Therefore, what we do is to emit a coverage file for every binary which contains only the files we collect coverage for with no covered lines. The baseline coverage file for a target is at bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat . It is also generated for binaries and libraries in addition to tests if you pass the --nobuild_tests_only flag to Bazel.

Baseline coverage is currently broken.

We track two groups of files for coverage collection for each rule: the set of instrumented files and the set of instrumentation metadata files.

The set of instrumented files is just that, a set of files to instrument. For online coverage runtimes, this can be used at runtime to decide which files to instrument. It is also used to implement baseline coverage.

The set of instrumentation metadata files is the set of extra files a test needs to generate the LCOV files Bazel requires from it. In practice, this consists of runtime-specific files; for example, gcc emits .gcno files during compilation. These are added to the set of inputs of test actions if coverage mode is enabled.

Whether or not coverage is being collected is stored in the BuildConfiguration . This is handy because it is an easy way to change the test action and the action graph depending on this bit, but it also means that if this bit is flipped, all targets need to be re-analyzed (some languages, such as C++ require different compiler options to emit code that can collect coverage, which mitigates this issue somewhat, since then a re-analysis is needed anyway).

The coverage support files are depended on through labels in an implicit dependency so that they can be overridden by the invocation policy, which allows them to differ between the different versions of Bazel. Ideally, these differences would be removed, and we standardized on one of them.

We also generate a "coverage report" which merges the coverage collected for every test in a Bazel invocation. This is handled by CoverageReportActionFactory and is called from BuildView.createResult() . It gets access to the tools it needs by looking at the :coverage_report_generator attribute of the first test that is executed.

The query engine

Bazel has a little language used to ask it various things about various graphs. The following query kinds are provided:

  • bazel query is used to investigate the target graph
  • bazel cquery is used to investigate the configured target graph
  • bazel aquery is used to investigate the action graph

Each of these is implemented by subclassing AbstractBlazeQueryEnvironment . Additional additional query functions can be done by subclassing QueryFunction . In order to allow streaming query results, instead of collecting them to some data structure, a query2.engine.Callback is passed to QueryFunction , which calls it for results it wants to return.

The result of a query can be emitted in various ways: labels, labels and rule classes, XML, protobuf and so on. These are implemented as subclasses of OutputFormatter .

A subtle requirement of some query output formats (proto, definitely) is that Bazel needs to emit _all _the information that package loading provides so that one can diff the output and determine whether a particular target has changed. As a consequence, attribute values need to be serializable, which is why there are only so few attribute types without any attributes having complex Starlark values. The usual workaround is to use a label, and attach the complex information to the rule with that label. It's not a very satisfying workaround and it would be very nice to lift this requirement.

The module system

Bazel can be extended by adding modules to it. Each module must subclass BlazeModule (the name is a relic of the history of Bazel when it used to be called Blaze) and gets information about various events during the execution of a command.

They are mostly used to implement various pieces of "non-core" functionality that only some versions of Bazel (such as the one we use at Google) need:

  • Interfaces to remote execution systems
  • New commands

The set of extension points BlazeModule offers is somewhat haphazard. Don't use it as an example of good design principles.

The event bus

The main way BlazeModules communicate with the rest of Bazel is by an event bus ( EventBus ): a new instance is created for every build, various parts of Bazel can post events to it and modules can register listeners for the events they are interested in. For example, the following things are represented as events:

  • The list of build targets to be built has been determined ( TargetParsingCompleteEvent )
  • The top-level configurations have been determined ( BuildConfigurationEvent )
  • A target was built, successfully or not ( TargetCompleteEvent )
  • A test was run ( TestAttempt , TestSummary )

Some of these events are represented outside of Bazel in the Build Event Protocol (they are BuildEvent s). This allows not only BlazeModule s, but also things outside the Bazel process to observe the build. They are accessible either as a file that contains protocol messages or Bazel can connect to a server (called the Build Event Service) to stream events.

This is implemented in the build.lib.buildeventservice and build.lib.buildeventstream Java packages.

External repositories

Whereas Bazel was originally designed to be used in a monorepo (a single source tree containing everything one needs to build), Bazel lives in a world where this is not necessarily true. "External repositories" are an abstraction used to bridge these two worlds: they represent code that is necessary for the build but is not in the main source tree.

The WORKSPACE file

The set of external repositories is determined by parsing the WORKSPACE file. For example, a declaration like this:

    local_repository(name="foo", path="/foo/bar")

Results in the repository called @foo being available. Where this gets complicated is that one can define new repository rules in Starlark files, which can then be used to load new Starlark code, which can be used to define new repository rules and so on…

To handle this case, the parsing of the WORKSPACE file (in WorkspaceFileFunction ) is split up into chunks delineated by load() statements. The chunk index is indicated by WorkspaceFileKey.getIndex() and computing WorkspaceFileFunction until index X means evaluating it until the Xth load() statement.

Fetching repositories

Before the code of the repository is available to Bazel, it needs to be fetched . This results in Bazel creating a directory under $OUTPUT_BASE/external/<repository name> .

Fetching the repository happens in the following steps:

  1. PackageLookupFunction realizes that it needs a repository and creates a RepositoryName as a SkyKey , which invokes RepositoryLoaderFunction
  2. RepositoryLoaderFunction forwards the request to RepositoryDelegatorFunction for unclear reasons (the code says it's to avoid re-downloading things in case of Skyframe restarts, but it's not a very solid reasoning)
  3. RepositoryDelegatorFunction finds out the repository rule it's asked to fetch by iterating over the chunks of the WORKSPACE file until the requested repository is found
  4. The appropriate RepositoryFunction is found that implements the repository fetching; it's either the Starlark implementation of the repository or a hard-coded map for repositories that are implemented in Java.

There are various layers of caching since fetching a repository can be very expensive:

  1. There is a cache for downloaded files that is keyed by their checksum ( RepositoryCache ). This requires the checksum to be available in the WORKSPACE file, but that's good for hermeticity anyway. This is shared by every Bazel server instance on the same workstation, regardless of which workspace or output base they are running in.
  2. A "marker file" is written for each repository under $OUTPUT_BASE/external that contains a checksum of the rule that was used to fetch it. If the Bazel server restarts but the checksum does not change, it's not re-fetched. This is implemented in RepositoryDelegatorFunction.DigestWriter .
  3. The --distdir command line option designates another cache that is used to look up artifacts to be downloaded. This is useful in enterprise settings where Bazel should not fetch random things from the Internet. This is implemented by DownloadManager .

Once a repository is downloaded, the artifacts in it are treated as source artifacts. This poses a problem because Bazel usually checks for up-to-dateness of source artifacts by calling stat() on them, and these artifacts are also invalidated when the definition of the repository they are in changes. Thus, FileStateValue s for an artifact in an external repository need to depend on their external repository. This is handled by ExternalFilesHelper .

Managed directories

Sometimes, external repositories need to modify files under the workspace root (such as a package manager that houses the downloaded packages in a subdirectory of the source tree). This is at odds with the assumption Bazel makes that source files are only modified by the user and not by itself and allows packages to refer to every directory under the workspace root. In order to make this kind of external repository work, Bazel does two things:

  1. Allows the user to specify subdirectories of the workspace Bazel is not allowed to reach into. They are listed in a file called .bazelignore and the functionality is implemented in BlacklistedPackagePrefixesFunction .
  2. We encode the mapping from the subdirectory of the workspace to the external repository it is handled by into ManagedDirectoriesKnowledge and handle FileStateValue s referring to them in the same way as those for regular external repositories.

Repository mappings

It can happen that multiple repositories want to depend on the same repository, but in different versions (this is an instance of the "diamond dependency problem"). For example, if two binaries in separate repositories in the build want to depend on Guava, they will presumably both refer to Guava with labels starting @guava// and expect that to mean different versions of it.

Therefore, Bazel allows one to re-map external repository labels so that the string @guava// can refer to one Guava repository (such as @guava1// ) in the repository of one binary and another Guava repository (such as @guava2// ) the the repository of the other.

Alternatively, this can also be used to join diamonds. If a repository depends on @guava1// , and another depends on @guava2// , repository mapping allows one to re-map both repositories to use a canonical @guava// repository.

The mapping is specified in the WORKSPACE file as the repo_mapping attribute of individual repository definitions. It then appears in Skyframe as a member of WorkspaceFileValue , where it is plumbed to:

  • Package.Builder.repositoryMapping which is used to transform label-valued attributes of rules in the package by RuleClass.populateRuleAttributeValues()
  • Package.repositoryMapping which is used in the analysis phase (for resolving things like $(location) which are not parsed in the loading phase)
  • BzlLoadFunction for resolving labels in load() statements

JNI bits

The server of Bazel is_ mostly _written in Java. The exception is the parts that Java cannot do by itself or couldn't do by itself when we implemented it. This is mostly limited to interaction with the file system, process control and various other low-level things.

The C++ code lives under src/main/native and the Java classes with native methods are:

  • NativePosixFiles and NativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperations and WindowsFileProcesses
  • com.google.devtools.build.lib.platform

Console output

Emitting console output seems like a simple thing, but the confluence of running multiple processes (sometimes remotely), fine-grained caching, the desire to have a nice and colorful terminal output and having a long-running server makes it non-trivial.

Right after the RPC call comes in from the client, two RpcOutputStream instances are created (for stdout and stderr) that forward the data printed into them to the client. These are then wrapped in an OutErr (an (stdout, stderr) pair). Anything that needs to be printed on the console goes through these streams. Then these streams are handed over to BlazeCommandDispatcher.execExclusively() .

Output is by default printed with ANSI escape sequences. When these are not desired ( --color=no ), they are stripped by an AnsiStrippingOutputStream . In addition, System.out and System.err are redirected to these output streams. This is so that debugging information can be printed using System.err.println() and still end up in the terminal output of the client (which is different from that of the server). Care is taken that if a process produces binary output (such as bazel query --output=proto ), no munging of stdout takes place.

Short messages (errors, warnings and the like) are expressed through the EventHandler interface. Notably, these are different from what one posts to the EventBus (this is confusing). Each Event has an EventKind (error, warning, info, and a few others) and they may have a Location (the place in the source code that caused the event to happen).

Some EventHandler implementations store the events they received. This is used to replay information to the UI caused by various kinds of cached processing, for example, the warnings emitted by a cached configured target.

Some EventHandler s also allow posting events that eventually find their way to the event bus (regular Event s do _not _appear there). These are implementations of ExtendedEventHandler and their main use is to replay cached EventBus events. These EventBus events all implement Postable , but not everything that is posted to EventBus necessarily implements this interface; only those that are cached by an ExtendedEventHandler (it would be nice and most of the things do; it's not enforced, though)

Terminal output is mostly emitted through UiEventHandler , which is responsible for all the fancy output formatting and progress reporting Bazel does. It has two inputs:

  • The event bus
  • The event stream piped into it through Reporter

The only direct connection the command execution machinery (for example the rest of Bazel) has to the RPC stream to the client is through Reporter.getOutErr() , which allows direct access to these streams. It's only used when a command needs to dump large amounts of possible binary data (such as bazel query ).

Profiling Bazel

Bazel is fast. Bazel is also slow, because builds tend to grow until just the edge of what's bearable. For this reason, Bazel includes a profiler which can be used to profile builds and Bazel itself. It's implemented in a class that's aptly named Profiler . It's turned on by default, although it records only abridged data so that its overhead is tolerable; The command line --record_full_profiler_data makes it record everything it can.

It emits a profile in the Chrome profiler format; it's best viewed in Chrome. It's data model is that of task stacks: one can start tasks and end tasks and they are supposed to be neatly nested within each other. Each Java thread gets its own task stack. TODO: How does this work with actions and continuation-passing style?

The profiler is started and stopped in BlazeRuntime.initProfiler() and BlazeRuntime.afterCommand() respectively and attempts to be live for as long as possible so that we can profile everything. To add something to the profile, call Profiler.instance().profile() . It returns a Closeable , whose closure represents the end of the task. It's best used with try-with-resources statements.

We also do rudimentary memory profiling in MemoryProfiler . It's also always on and it mostly records maximum heap sizes and GC behavior.

Testing Bazel

Bazel has two main kinds of tests: ones that observe Bazel as a "black box" and ones that only run the analysis phase. We call the former "integration tests" and the latter "unit tests", although they are more like integration tests that are, well, less integrated. We also have some actual unit tests, where they are necessary.

Of integration tests, we have two kinds:

  1. Ones implemented using a very elaborate bash test framework under src/test/shell
  2. Ones implemented in Java. These are implemented as subclasses of BuildIntegrationTestCase

BuildIntegrationTestCase is the preferred integration testing framework as it is well-equipped for most testing scenarios. As it is a Java framework, it provides debuggability and seamless integration with many common development tools. There are many examples of BuildIntegrationTestCase classes in the Bazel repository.

Analysis tests are implemented as subclasses of BuildViewTestCase . There is a scratch file system you can use to write BUILD files, then various helper methods can request configured targets, change the configuration and assert various things about the result of the analysis.