1use std::io::{BufRead, BufReader, BufWriter, Write};
16
17use anyhow::{Context, Result};
18use clap::{Parser, Subcommand, ValueEnum};
19use console::style;
20use dialoguer::MultiSelect;
21use enum_iterator::{Sequence, all};
22use fs_err::OpenOptions;
23use itertools::Itertools;
24
25#[derive(Parser)]
26#[clap(author, version, about, long_about = None)]
27#[clap(propagate_version = true)]
28#[clap(infer_subcommands = true)]
29pub struct RiseDevConfigOpts {
30 #[clap(subcommand)]
31 command: Option<Commands>,
32 #[clap(short, long)]
33 file: String,
34}
35
36#[derive(Subcommand)]
37#[clap(infer_subcommands = true)]
38enum Commands {
39 Enable {
41 #[clap(value_enum)]
43 component: Components,
44 },
45 Disable {
47 #[clap(value_enum)]
49 component: Components,
50 },
51 Default,
53}
54
55#[expect(clippy::enum_variant_names)]
56#[derive(Clone, Copy, Debug, Sequence, PartialEq, Eq, ValueEnum)]
57pub enum Components {
58 #[clap(name = "minio")]
59 Minio,
60 Lakekeeper,
61 Hdfs,
62 PrometheusAndGrafana,
63 Tracing,
64 RustComponents,
65 UseSystem,
66 BuildConnectorNode,
67 Dashboard,
68 Release,
69 Sanitizer,
70 DynamicLinking,
71 HummockTrace,
72 Coredump,
73 NoBacktrace,
74 Udf,
75 NoDefaultFeatures,
76 NoHeavyConnectors,
77 Moat,
78 DataFusion,
79 Adbc,
80}
81
82impl Components {
83 pub fn title(&self) -> String {
84 match self {
85 Self::Minio => "[Component] Hummock: MinIO + MinIO-CLI",
86 Self::Lakekeeper => "[Component] Apache Iceberg: Lakekeeper REST Catalog",
87 Self::Hdfs => "[Component] Hummock: Hdfs Backend",
88 Self::PrometheusAndGrafana => "[Component] Metrics: Prometheus + Grafana",
89 Self::BuildConnectorNode => "[Build] Build RisingWave Connector (Java)",
90 Self::RustComponents => "[Build] Rust components",
91 Self::UseSystem => "[Build] Use system RisingWave",
92 Self::Dashboard => "[Build] Dashboard",
93 Self::Tracing => "[Component] Tracing: Grafana Tempo",
94 Self::Release => "[Build] Enable release mode",
95 Self::Sanitizer => "[Build] Enable sanitizer",
96 Self::DynamicLinking => "[Build] Enable dynamic linking",
97 Self::HummockTrace => "[Build] Hummock Trace",
98 Self::Coredump => "[Runtime] Enable coredump",
99 Self::NoBacktrace => "[Runtime] Disable backtrace",
100 Self::Udf => "[Build] Enable UDF",
101 Self::NoDefaultFeatures => "[Build] Disable default features",
102 Self::NoHeavyConnectors => "[Build] Disable heavyweight connectors",
103 Self::Moat => "[Component] Enable Moat",
104 Self::DataFusion => "[Build] Enable DataFusion",
105 Self::Adbc => "[Component] ADBC Snowflake Driver",
106 }
107 .into()
108 }
109
110 pub fn description(&self) -> String {
111 match self {
112 Self::Minio => {
113 "
114Required by Hummock state store."
115 }
116 Self::Lakekeeper => {
117 "
118Required if you want to use Apache Iceberg REST Catalog.
119Provides catalog and metadata management for Apache Iceberg tables."
120 }
121 Self::Hdfs => {
122 "
123Required by Hummock state store."
124 }
125 Self::PrometheusAndGrafana => {
126 "
127Required if you want to view metrics."
128 }
129 Self::RustComponents => {
130 "
131Required if you want to build compute-node and meta-node.
132Otherwise you will need to enable `USE_SYSTEM_RISINGWAVE`, or
133manually download a binary and copy it to RiseDev directory."
134 }
135 Self::UseSystem => {
136 "
137Use the RisingWave installed in the PATH, instead of building it
138from source. This implies `ENABLE_BUILD_RUST` to be false.
139 "
140 }
141 Self::Dashboard => {
142 "
143Required if you want to build dashboard from source.
144This is generally not the option you want to use to develop the
145dashboard. Instead, directly run `pnpm run dev` in the dashboard
146directory to start the development server, set the API endpoint
147to a running RisingWave cluster in the settings page.
148"
149 }
150 Self::Tracing => {
151 "
152Required if you want to use tracing. This option will help
153you download Grafana Tempo."
154 }
155 Self::Release => {
156 "
157Build RisingWave in release mode"
158 }
159 Self::Sanitizer => {
160 "
161With this option enabled, RiseDev will build Rust components
162with thread sanitizer. The built binaries will be at
163`target/<arch-triple>/(debug|release)` instead of simply at
164`target/debug`. RiseDev will help link binaries when starting
165a dev cluster.
166"
167 }
168 Self::BuildConnectorNode => {
169 "
170Required if you want to build Connector Node from source locally.
171 "
172 }
173 Self::DynamicLinking => {
174 "
175With this option enabled, RiseDev will use dynamic linking when
176building Rust components. This can speed up the build process,
177but you might need the expertise to install dependencies correctly.
178 "
179 }
180 Self::HummockTrace => {
181 "
182With this option enabled, RiseDev will enable tracing for Hummock.
183See storage/hummock_trace for details.
184 "
185 }
186 Self::Coredump => {
187 "
188With this option enabled, RiseDev will unlimit the size of core
189files before launching RisingWave. On Apple Silicon platforms,
190the binaries will also be codesigned with `get-task-allow` enabled.
191As a result, RisingWave will dump the core on panics.
192 "
193 }
194 Self::NoBacktrace => {
195 "
196With this option enabled, RiseDev will not set `RUST_BACKTRACE` when launching nodes.
197 "
198 }
199 Self::Udf => {
200 "
201Add --features udf to build command (by default disabled).
202Required if you want to support UDF."
203 }
204 Self::NoDefaultFeatures => {
205 "
206Add --no-default-features to build command.
207Currently, default features are: rw-static-link, all-connectors
208"
209 }
210 Self::NoHeavyConnectors => {
211 "
212Exclude heavyweight connectors, such as LanceDB, to reduce build size.
213Other default connectors remain enabled unless default features are disabled."
214 }
215 Self::Moat => {
216 "
217Enable Moat as distributed hybrid cache service."
218 }
219 Self::DataFusion => {
220 "
221Enable DataFusion as the optional query engine for Iceberg tables."
222 }
223 Self::Adbc => {
224 "
225Enable ADBC (Arrow Database Connectivity) Snowflake driver support.
226Required if you want to use ADBC Snowflake source.
227This will download the ADBC Snowflake driver shared library (.so/.dylib)."
228 }
229 }
230 .into()
231 }
232
233 pub fn from_env(env: impl AsRef<str>) -> Option<Self> {
234 match env.as_ref() {
235 "ENABLE_MINIO" => Some(Self::Minio),
236 "ENABLE_LAKEKEEPER" => Some(Self::Lakekeeper),
237 "ENABLE_HDFS" => Some(Self::Hdfs),
238 "ENABLE_PROMETHEUS_GRAFANA" => Some(Self::PrometheusAndGrafana),
239 "ENABLE_BUILD_RUST" => Some(Self::RustComponents),
240 "USE_SYSTEM_RISINGWAVE" => Some(Self::UseSystem),
241 "ENABLE_BUILD_DASHBOARD" => Some(Self::Dashboard),
242 "ENABLE_COMPUTE_TRACING" => Some(Self::Tracing),
243 "ENABLE_RELEASE_PROFILE" => Some(Self::Release),
244 "ENABLE_DYNAMIC_LINKING" => Some(Self::DynamicLinking),
245 "ENABLE_SANITIZER" => Some(Self::Sanitizer),
246 "ENABLE_BUILD_RW_CONNECTOR" => Some(Self::BuildConnectorNode),
247 "ENABLE_HUMMOCK_TRACE" => Some(Self::HummockTrace),
248 "ENABLE_COREDUMP" => Some(Self::Coredump),
249 "DISABLE_BACKTRACE" => Some(Self::NoBacktrace),
250 "ENABLE_UDF" => Some(Self::Udf),
251 "DISABLE_DEFAULT_FEATURES" => Some(Self::NoDefaultFeatures),
252 "DISABLE_HEAVY_CONNECTORS" => Some(Self::NoHeavyConnectors),
253 "ENABLE_MOAT" => Some(Self::Moat),
254 "ENABLE_DATAFUSION" => Some(Self::DataFusion),
255 "ENABLE_ADBC" => Some(Self::Adbc),
256 _ => None,
257 }
258 }
259
260 pub fn env(&self) -> String {
261 match self {
262 Self::Minio => "ENABLE_MINIO",
263 Self::Lakekeeper => "ENABLE_LAKEKEEPER",
264 Self::Hdfs => "ENABLE_HDFS",
265 Self::PrometheusAndGrafana => "ENABLE_PROMETHEUS_GRAFANA",
266 Self::RustComponents => "ENABLE_BUILD_RUST",
267 Self::UseSystem => "USE_SYSTEM_RISINGWAVE",
268 Self::Dashboard => "ENABLE_BUILD_DASHBOARD",
269 Self::Tracing => "ENABLE_COMPUTE_TRACING",
270 Self::Release => "ENABLE_RELEASE_PROFILE",
271 Self::Sanitizer => "ENABLE_SANITIZER",
272 Self::BuildConnectorNode => "ENABLE_BUILD_RW_CONNECTOR",
273 Self::DynamicLinking => "ENABLE_DYNAMIC_LINKING",
274 Self::HummockTrace => "ENABLE_HUMMOCK_TRACE",
275 Self::Coredump => "ENABLE_COREDUMP",
276 Self::NoBacktrace => "DISABLE_BACKTRACE",
277 Self::Udf => "ENABLE_UDF",
278 Self::NoDefaultFeatures => "DISABLE_DEFAULT_FEATURES",
279 Self::NoHeavyConnectors => "DISABLE_HEAVY_CONNECTORS",
280 Self::Moat => "ENABLE_MOAT",
281 Self::DataFusion => "ENABLE_DATAFUSION",
282 Self::Adbc => "ENABLE_ADBC",
283 }
284 .into()
285 }
286
287 pub fn default_enabled() -> &'static [Self] {
288 &[Self::RustComponents, Self::NoHeavyConnectors]
289 }
290}
291
292fn configure(chosen: &[Components]) -> Result<Option<Vec<Components>>> {
293 println!("=== Configure RiseDev ===");
294
295 let all_components = all::<Components>().collect_vec();
296
297 const ITEMS_PER_PAGE: usize = 6;
298
299 let items = all_components
300 .iter()
301 .map(|c| {
302 let title = c.title();
303 let desc = style(
304 ("\n".to_owned() + c.description().trim())
305 .split('\n')
306 .join("\n "),
307 )
308 .dim();
309
310 (format!("{title}{desc}",), chosen.contains(c))
311 })
312 .collect_vec();
313
314 let Some(chosen_indices) = MultiSelect::new()
315 .with_prompt(
316 format!(
317 "RiseDev includes several components. You can select the ones you need, so as to reduce build time\n\n{}: navigate\n{}: confirm and save {}: quit without saving\n\nPick items with {}",
318 style("↑ / ↓ / ← / → ").reverse(),
319 style("Enter").reverse(),
320 style("Esc / q").reverse(),
321 style("Space").reverse(),
322 )
323 )
324 .items_checked(items)
325 .max_length(ITEMS_PER_PAGE)
326 .interact_opt()? else {
327 return Ok(None);
328 };
329
330 let chosen = chosen_indices
331 .into_iter()
332 .map(|i| all_components[i])
333 .collect_vec();
334
335 Ok(Some(chosen))
336}
337
338fn main() -> Result<()> {
339 let opts = RiseDevConfigOpts::parse();
340 let file_path = opts.file;
341
342 let chosen = {
343 match OpenOptions::new().read(true).open(&file_path) {
344 Ok(file) => {
345 let reader = BufReader::new(file);
346 let mut enabled = vec![];
347 for line in reader.lines() {
348 let line = line?;
349 if line.trim().is_empty() || line.trim().starts_with('#') {
350 continue;
351 }
352 let Some((component, val)) = line.split_once('=') else {
353 println!("invalid config line {}, discarded", line);
354 continue;
355 };
356 if component == "RISEDEV_CONFIGURED" {
357 continue;
358 }
359 match Components::from_env(component) {
360 Some(component) => {
361 if val == "true" {
362 enabled.push(component);
363 }
364 }
365 None => {
366 println!("unknown configure {}, discarded", component);
367 continue;
368 }
369 }
370 }
371 enabled
372 }
373 _ => {
374 println!(
375 "RiseDev component config not found, generating {}",
376 file_path
377 );
378 Components::default_enabled().to_vec()
379 }
380 }
381 };
382
383 let chosen = match &opts.command {
384 Some(Commands::Default) => {
385 println!("Using default config");
386 Components::default_enabled().to_vec()
387 }
388 Some(Commands::Enable { component }) => {
389 let mut chosen = chosen;
390 chosen.push(*component);
391 chosen
392 }
393 Some(Commands::Disable { component }) => {
394 chosen.into_iter().filter(|x| x != component).collect()
395 }
396 None => match configure(&chosen)? {
397 Some(chosen) => chosen,
398 None => {
399 println!("Quit without saving");
400 println!("=========================");
401 return Ok(());
402 }
403 },
404 };
405
406 println!("=== Enabled Components ===");
407 for component in all::<Components>() {
408 println!(
409 "{}: {}",
410 component.title(),
411 if chosen.contains(&component) {
412 style("enabled").green()
413 } else {
414 style("disabled").dim()
415 }
416 );
417 }
418
419 println!("Configuration saved at {}", file_path);
420 println!("=========================");
421
422 let mut file = BufWriter::new(
423 OpenOptions::new()
424 .write(true)
425 .truncate(true)
426 .create(true)
427 .open(&file_path)
428 .context(format!("failed to open component config at {}", file_path))?,
429 );
430
431 writeln!(file, "RISEDEV_CONFIGURED=true")?;
432 writeln!(file)?;
433
434 for component in all::<Components>() {
435 writeln!(file, "# {}", component.title())?;
436 writeln!(
437 file,
438 "# {}",
439 component.description().trim().split('\n').join("\n# ")
440 )?;
441 if chosen.contains(&component) {
442 writeln!(file, "{}=true", component.env())?;
443 } else {
444 writeln!(file, "# {}=true", component.env())?;
445 }
446 writeln!(file)?;
447 }
448
449 file.flush()?;
450
451 println!(
452 "RiseDev will {} the components you've enabled.",
453 style("only download").bold()
454 );
455 println!(
456 "If you want to use these components, please {} in {} to start that component.",
457 style("modify the cluster config").yellow().bold(),
458 style("risedev.yml").bold(),
459 );
460 println!("See CONTRIBUTING.md or RiseDev's readme for more information.");
461
462 Ok(())
463}