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