Skip to content

Commit

Permalink
Add a log external-configs command
Browse files Browse the repository at this point in the history
Summary: Expose a command to log external-configs.

Reviewed By: JakobDegen

Differential Revision: D70094495

fbshipit-source-id: 17059ba9b6653be485ca8bb4ee7c69acf7f0b424
  • Loading branch information
ezgicicek authored and facebook-github-bot committed Feb 25, 2025
1 parent c016235 commit bab088b
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 0 deletions.
3 changes: 3 additions & 0 deletions app/buck2_client/src/commands/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod critical_path;
pub(crate) mod debug_replay;
pub(crate) mod debug_what_ran;
mod diff;
mod external_configs;
pub(crate) mod options;
pub(crate) mod path_log;
mod replay;
Expand Down Expand Up @@ -95,6 +96,7 @@ pub enum LogCommand {
Summary(summary::SummaryCommand),
#[clap(subcommand)]
Diff(diff::DiffCommand),
ExternalConfigs(external_configs::ExternalConfigsCommand),
}

impl LogCommand {
Expand All @@ -113,6 +115,7 @@ impl LogCommand {
Self::ShowUser(cmd) => cmd.exec(matches, ctx),
Self::Summary(cmd) => cmd.exec(matches, ctx),
Self::Diff(cmd) => cmd.exec(matches, ctx),
Self::ExternalConfigs(cmd) => cmd.exec(matches, ctx),
}
}

Expand Down
205 changes: 205 additions & 0 deletions app/buck2_client/src/commands/log/external_configs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under both the MIT license found in the
* LICENSE-MIT file in the root directory of this source tree and the Apache
* License, Version 2.0 found in the LICENSE-APACHE file in the root directory
* of this source tree.
*/

use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::common::BuckArgMatches;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_error::conversion::from_any_with_tag;
use buck2_event_log::stream_value::StreamValue;
use serde::Serialize;
use tokio_stream::StreamExt;

use crate::commands::log::options::EventLogOptions;
use crate::commands::log::transform_format;
use crate::commands::log::LogCommandOutputFormat;
use crate::commands::log::LogCommandOutputFormatWithWriter;

/// Display the values and origins of external configs for a selected command.
///
/// Buckconfigs are computed by joining together values from various inputs (repo, well-known directories, CLI flags). Each of these is
/// logged in the given order, with later components overriding earlier ones. For config files originating from the repo (i.e. project-relative paths), except .buckconfig.local,
/// we log the path, not the actual values.
#[derive(Debug, clap::Parser)]
pub struct ExternalConfigsCommand {
#[clap(flatten)]
event_log: EventLogOptions,
#[clap(
long,
help = "Which output format to use for this command",
default_value = "tabulated",
ignore_case = true,
value_enum
)]
format: LogCommandOutputFormat,
}

impl ExternalConfigsCommand {
pub fn exec(self, _matches: BuckArgMatches<'_>, ctx: ClientCommandContext<'_>) -> ExitResult {
let Self { event_log, format } = self;

ctx.instant_command_no_log("log-external-configs", |ctx| async move {
let log_path = event_log.get(&ctx).await?;

let (invocation, mut events) = log_path.unpack_stream().await?;
buck2_client_ctx::eprintln!(
"Showing external configs from: {}",
invocation.display_command_line()
)?;

while let Some(event) = events.try_next().await? {
match event {
StreamValue::Event(event) => match event.data {
Some(buck2_data::buck_event::Data::Instant(instant)) => {
match instant.data {
Some(buck2_data::instant_event::Data::BuckconfigInputValues(
configs,
)) => {
log_external_configs(&configs.components, format.clone())?;
}
_ => {}
}
}
_ => {}
},
_ => {}
}
}

buck2_error::Ok(())
})
.into()
}
}

#[derive(Serialize)]
struct ExternalConfigValueEntry<'a> {
section: &'a str,
key: &'a str,
value: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
cell: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
origin: Option<&'a str>,
}

#[derive(Serialize)]
struct ExternalConfigFileEntry<'a> {
path: &'a str,
origin: &'a str,
}

fn write_config_value<'a>(
config_value: &'a buck2_data::ConfigValue,
mut log_writer: &mut LogCommandOutputFormatWithWriter,
) -> buck2_error::Result<()> {
let external_config = ExternalConfigValueEntry {
section: &config_value.section,
key: &config_value.key,
value: &config_value.value,
cell: config_value.cell.as_deref(),
origin: if config_value.is_cli {
Some("cli")
} else {
None
},
};

match &mut log_writer {
LogCommandOutputFormatWithWriter::Tabulated(writer) => {
writeln!(
writer,
"{}.{} = {}\t{}\t{}",
external_config.section,
external_config.key,
external_config.value,
external_config
.cell
.map_or("".to_owned(), |cell| format!("({})", cell)),
external_config.origin.unwrap_or_default(),
)?;
}
LogCommandOutputFormatWithWriter::Json(writer) => {
serde_json::to_writer(&mut **writer, &external_config)?;
writer.write_all("\n".as_bytes())?;
}
LogCommandOutputFormatWithWriter::Csv(writer) => {
writer
.serialize(external_config)
.map_err(|e| from_any_with_tag(e, buck2_error::ErrorTag::Tier0))?;
}
}
Ok(())
}

fn write_config_values(
configs: &[buck2_data::ConfigValue],
mut log_writer: &mut LogCommandOutputFormatWithWriter,
) -> buck2_error::Result<()> {
configs
.iter()
.try_for_each(|config_value| write_config_value(config_value, &mut log_writer))
}

fn write_config_file(
path: &str,
mut log_writer: &mut LogCommandOutputFormatWithWriter,
) -> buck2_error::Result<()> {
let origin = "config-file";
let config_file = ExternalConfigFileEntry { path, origin };
match &mut log_writer {
LogCommandOutputFormatWithWriter::Tabulated(writer) => {
writeln!(writer, "{}\t\t{}", path, origin)?;
}
LogCommandOutputFormatWithWriter::Json(writer) => {
serde_json::to_writer(&mut **writer, &config_file)?;
writer.write_all("\n".as_bytes())?;
}
LogCommandOutputFormatWithWriter::Csv(writer) => {
writer
.serialize(config_file)
.map_err(|e| from_any_with_tag(e, buck2_error::ErrorTag::Tier0))?;
}
}
Ok(())
}

fn log_external_configs(
components: &[buck2_data::BuckconfigComponent],
format: LogCommandOutputFormat,
) -> buck2_error::Result<()> {
buck2_client_ctx::stdio::print_with_writer::<buck2_error::Error, _>(|w| {
let mut log_writer = transform_format(format, w);

for component in components {
use buck2_data::buckconfig_component::Data;
use buck2_data::config_file::Data as CData;
match &component.data {
Some(Data::ConfigValue(config_value)) => {
write_config_value(config_value, &mut log_writer)?;
}
Some(Data::ConfigFile(config_file)) => config_file
.data
.as_ref()
.into_iter()
.try_for_each(|data| match data {
CData::ProjectRelativePath(p) => write_config_file(&p, &mut log_writer),
CData::GlobalExternalConfig(external_config_values) => {
write_config_values(&external_config_values.values, &mut log_writer)
}
})?,

Some(Data::GlobalExternalConfigFile(external_config_file)) => {
write_config_values(&external_config_file.values, &mut log_writer)?
}
_ => {}
}
}
Ok(())
})
}
83 changes: 83 additions & 0 deletions tests/core/build/test_external_buckconfigs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@

import json
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

from buck2.tests.e2e_util.api.buck import Buck
from buck2.tests.e2e_util.buck_workspace import buck_test
from buck2.tests.e2e_util.helper.golden import golden
from buck2.tests.e2e_util.helper.utils import filter_events, read_invocation_record


Expand Down Expand Up @@ -231,3 +234,83 @@ async def test_previous_command_with_mismatched_config(
)
assert len(previous_invalidating_command) == 1
assert previous_invalidating_command[0]["trace_id"] == trace_id


@dataclass
class ExternalConfigsLog:
descriptor: str
cell: Optional[str] = None
origin: Optional[str] = None


@buck_test()
async def test_log_external_configs(buck: Buck) -> None:
await buck.build(
"@root//mode/my_mode",
"//:test",
"-c",
"my_section.my_key=my_value",
"-c",
"my_section.my_key=my_new_value",
)

external_configs = (await buck.log("external-configs")).stdout.strip().splitlines()
external_configs = [e.split("\t") for e in external_configs]

external_configs = [
ExternalConfigsLog(
e[0], # section.key = value
e[1] if len(e) > 1 else None, # cell
e[2] if len(e) > 2 else None, # origin
)
for e in external_configs
]
expected = [
# Our tests inject file_watcher to external configs in test setup stage
ExternalConfigsLog(
descriptor="buck2.file_watcher = fs_hash_crawler",
),
ExternalConfigsLog(
descriptor="my_mode.bcfg",
origin="config-file",
),
ExternalConfigsLog(
descriptor="my_section.my_key = my_value",
origin="cli",
),
# We don't override but just display the input as it is provided by the cli
ExternalConfigsLog(
descriptor="my_section.my_key = my_new_value",
origin="cli",
),
]
assert len(external_configs) == 4

for s, e in zip(external_configs, expected):
assert s.descriptor == e.descriptor
if s.origin != "":
assert s.origin == e.origin


@buck_test()
async def test_log_external_configs_json(buck: Buck) -> None:
await buck.build(
"@root//mode/my_mode",
"//:test",
"-c",
"my_section.my_key=my_value",
"-c",
"my_section.my_key=my_new_value",
)

external_configs = (
(await buck.log("external-configs", "--format", "json"))
.stdout.strip()
.splitlines()
)
external_configs = [json.loads(e) for e in external_configs]

golden(
output=json.dumps(external_configs, sort_keys=True, indent=2),
rel_path="events.golden.json",
)
25 changes: 25 additions & 0 deletions tests/core/build/test_external_buckconfigs_data/events.golden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This file is @generated, regenerate by re-running test with `-- --env BUCK2_UPDATE_GOLDEN=1` appended to the test command

[
{
"key": "file_watcher",
"section": "buck2",
"value": "fs_hash_crawler"
},
{
"origin": "config-file",
"path": "my_mode.bcfg"
},
{
"key": "my_key",
"origin": "cli",
"section": "my_section",
"value": "my_value"
},
{
"key": "my_key",
"origin": "cli",
"section": "my_section",
"value": "my_new_value"
}
]
Loading

0 comments on commit bab088b

Please sign in to comment.