Auto merge of #113026 - jieyouxu:run-make-v2, r=bjorn3

Introduce `run-make` V2 infrastructure, a `run_make_support` library and port over 2 tests as example

## Preface

See [issue #40713: Switch run-make tests from Makefiles to rust](https://github.com/rust-lang/rust/issues/40713) for more context.

## Basic Description of `run-make` V2

`run-make` V2 aims to eliminate the dependency on `make` and `Makefile`s for building `run-make`-style tests. Makefiles are replaced by *recipes* (`rmake.rs`). The current implementation runs `run-make` V2 tests in 3 steps:

1. We build the support library `run_make_support` which the `rmake.rs` recipes depend on as a tool lib.
2. We build the recipe `rmake.rs` and link in the support library.
3. We run the recipe to build and run the tests.

`rmake.rs` is basically a replacement for `Makefile`, and allows running arbitrary Rust code. The support library is built using cargo, and so can depend on external crates if desired.

The infrastructure implemented by this PR is very barebones, and is the minimally required infrastructure needed to build, run and pass the two example `run-make` tests ported over to the new infrastructure.

### Example `run-make` V2 test

```rs
// ignore-tidy-linelength

extern crate run_make_support;

use std::path::PathBuf;

use run_make_support::{aux_build, rustc};

fn main() {
    aux_build()
        .arg("--emit=metadata")
        .arg("stable.rs")
        .run();
    let mut stable_path = PathBuf::from(env!("TMPDIR"));
    stable_path.push("libstable.rmeta");
    let output = rustc()
        .arg("--emit=metadata")
        .arg("--extern")
        .arg(&format!("stable={}", &stable_path.to_string_lossy()))
        .arg("main.rs")
        .run();

    let stderr = String::from_utf8_lossy(&output.stderr);
    let version = include_str!(concat!(env!("S"), "/src/version"));
    let expected_string = format!("stable since {}", version.trim());
    assert!(stderr.contains(&expected_string));
}
```

## Follow Up Work

- [ ] Adjust rustc-dev-guide docs
This commit is contained in:
bors 2024-03-01 16:43:57 +00:00
commit 17edacef07
11 changed files with 583 additions and 32 deletions

View File

@ -3274,6 +3274,10 @@ dependencies = [
"serde_json",
]
[[package]]
name = "run_make_support"
version = "0.0.0"
[[package]]
name = "rust-demangler"
version = "0.0.1"

View File

@ -10,6 +10,7 @@ members = [
"src/tools/clippy",
"src/tools/clippy/clippy_dev",
"src/tools/compiletest",
"src/tools/run-make-support",
"src/tools/error_index_generator",
"src/tools/linkchecker",
"src/tools/lint-docs",

View File

@ -1327,6 +1327,52 @@ macro_rules! coverage_test_alias {
};
}
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
pub struct RunMakeSupport {
pub compiler: Compiler,
pub target: TargetSelection,
}
impl Step for RunMakeSupport {
type Output = PathBuf;
const DEFAULT: bool = true;
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
run.never()
}
fn make_run(run: RunConfig<'_>) {
let compiler = run.builder.compiler(run.builder.top_stage, run.build_triple());
run.builder.ensure(RunMakeSupport { compiler, target: run.build_triple() });
}
fn run(self, builder: &Builder<'_>) -> PathBuf {
builder.ensure(compile::Std::new(self.compiler, self.target));
let cargo = tool::prepare_tool_cargo(
builder,
self.compiler,
Mode::ToolStd,
self.target,
"build",
"src/tools/run-make-support",
SourceType::InTree,
&[],
);
let mut cargo = Command::from(cargo);
builder.run(&mut cargo);
let lib_name = "librun_make_support.rlib";
let lib = builder.tools_dir(self.compiler).join(&lib_name);
let cargo_out =
builder.cargo_out(self.compiler, Mode::ToolStd, self.target).join(&lib_name);
builder.copy(&cargo_out, &lib);
lib
}
}
default_test!(Ui { path: "tests/ui", mode: "ui", suite: "ui" });
default_test!(RunPassValgrind {
@ -1361,7 +1407,40 @@ host_test!(RustdocJson { path: "tests/rustdoc-json", mode: "rustdoc-json", suite
host_test!(Pretty { path: "tests/pretty", mode: "pretty", suite: "pretty" });
default_test!(RunMake { path: "tests/run-make", mode: "run-make", suite: "run-make" });
// Special-handling is needed for `run-make`, so don't use `default_test` for defining `RunMake`
// tests.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct RunMake {
pub compiler: Compiler,
pub target: TargetSelection,
}
impl Step for RunMake {
type Output = ();
const DEFAULT: bool = true;
const ONLY_HOSTS: bool = false;
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
run.suite_path("tests/run-make")
}
fn make_run(run: RunConfig<'_>) {
let compiler = run.builder.compiler(run.builder.top_stage, run.build_triple());
run.builder.ensure(RunMakeSupport { compiler, target: run.build_triple() });
run.builder.ensure(RunMake { compiler, target: run.target });
}
fn run(self, builder: &Builder<'_>) {
builder.ensure(Compiletest {
compiler: self.compiler,
target: self.target,
mode: "run-make",
suite: "run-make",
path: "tests/run-make",
compare_mode: None,
});
}
}
host_test!(RunMakeFullDeps {
path: "tests/run-make-fulldeps",

View File

@ -655,13 +655,21 @@ fn collect_tests_from_dir(
return Ok(());
}
if config.mode == Mode::RunMake && dir.join("Makefile").exists() {
let paths = TestPaths {
file: dir.to_path_buf(),
relative_dir: relative_dir_path.parent().unwrap().to_path_buf(),
};
tests.extend(make_test(config, cache, &paths, inputs, poisoned));
return Ok(());
if config.mode == Mode::RunMake {
if dir.join("Makefile").exists() && dir.join("rmake.rs").exists() {
return Err(io::Error::other(
"run-make tests cannot have both `Makefile` and `rmake.rs`",
));
}
if dir.join("Makefile").exists() || dir.join("rmake.rs").exists() {
let paths = TestPaths {
file: dir.to_path_buf(),
relative_dir: relative_dir_path.parent().unwrap().to_path_buf(),
};
tests.extend(make_test(config, cache, &paths, inputs, poisoned));
return Ok(());
}
}
// If we find a test foo/bar.rs, we have to build the
@ -733,8 +741,17 @@ fn make_test(
poisoned: &mut bool,
) -> Vec<test::TestDescAndFn> {
let test_path = if config.mode == Mode::RunMake {
// Parse directives in the Makefile
testpaths.file.join("Makefile")
if testpaths.file.join("rmake.rs").exists() && testpaths.file.join("Makefile").exists() {
panic!("run-make tests cannot have both `rmake.rs` and `Makefile`");
}
if testpaths.file.join("rmake.rs").exists() {
// Parse directives in rmake.rs.
testpaths.file.join("rmake.rs")
} else {
// Parse directives in the Makefile.
testpaths.file.join("Makefile")
}
} else {
PathBuf::from(&testpaths.file)
};

View File

@ -3570,6 +3570,17 @@ impl<'test> TestCx<'test> {
}
fn run_rmake_test(&self) {
let test_dir = &self.testpaths.file;
if test_dir.join("rmake.rs").exists() {
self.run_rmake_v2_test();
} else if test_dir.join("Makefile").exists() {
self.run_rmake_legacy_test();
} else {
self.fatal("failed to find either `rmake.rs` or `Makefile`")
}
}
fn run_rmake_legacy_test(&self) {
let cwd = env::current_dir().unwrap();
let src_root = self.config.src_base.parent().unwrap().parent().unwrap();
let src_root = cwd.join(&src_root);
@ -3737,6 +3748,238 @@ impl<'test> TestCx<'test> {
fs::remove_dir(path)
}
fn run_rmake_v2_test(&self) {
// For `run-make` V2, we need to perform 2 steps to build and run a `run-make` V2 recipe
// (`rmake.rs`) to run the actual tests. The support library is already built as a tool
// dylib and is available under `build/$TARGET/stageN-tools-bin/librun_make_support.rlib`.
//
// 1. We need to build the recipe `rmake.rs` and link in the support library.
// 2. We need to run the recipe to build and run the tests.
let cwd = env::current_dir().unwrap();
let src_root = self.config.src_base.parent().unwrap().parent().unwrap();
let src_root = cwd.join(&src_root);
let build_root = self.config.build_base.parent().unwrap().parent().unwrap();
let build_root = cwd.join(&build_root);
let tmpdir = cwd.join(self.output_base_name());
if tmpdir.exists() {
self.aggressive_rm_rf(&tmpdir).unwrap();
}
create_dir_all(&tmpdir).unwrap();
// HACK: assume stageN-target, we only want stageN.
let stage = self.config.stage_id.split('-').next().unwrap();
// First, we construct the path to the built support library.
let mut support_lib_path = PathBuf::new();
support_lib_path.push(&build_root);
support_lib_path.push(format!("{}-tools-bin", stage));
support_lib_path.push("librun_make_support.rlib");
let mut stage_std_path = PathBuf::new();
stage_std_path.push(&build_root);
stage_std_path.push(&stage);
stage_std_path.push("lib");
// Then, we need to build the recipe `rmake.rs` and link in the support library.
let recipe_bin =
tmpdir.join(if self.config.target.contains("windows") { "rmake.exe" } else { "rmake" });
let mut support_lib_deps = PathBuf::new();
support_lib_deps.push(&build_root);
support_lib_deps.push(format!("{}-tools", stage));
support_lib_deps.push(&self.config.host);
support_lib_deps.push("release");
support_lib_deps.push("deps");
let mut support_lib_deps_deps = PathBuf::new();
support_lib_deps_deps.push(&build_root);
support_lib_deps_deps.push(format!("{}-tools", stage));
support_lib_deps_deps.push("release");
support_lib_deps_deps.push("deps");
debug!(?support_lib_deps);
debug!(?support_lib_deps_deps);
let res = self.cmd2procres(
Command::new(&self.config.rustc_path)
.arg("-o")
.arg(&recipe_bin)
.arg(format!(
"-Ldependency={}",
&support_lib_path.parent().unwrap().to_string_lossy()
))
.arg(format!("-Ldependency={}", &support_lib_deps.to_string_lossy()))
.arg(format!("-Ldependency={}", &support_lib_deps_deps.to_string_lossy()))
.arg("--extern")
.arg(format!("run_make_support={}", &support_lib_path.to_string_lossy()))
.arg(&self.testpaths.file.join("rmake.rs"))
.env("TARGET", &self.config.target)
.env("PYTHON", &self.config.python)
.env("S", &src_root)
.env("RUST_BUILD_STAGE", &self.config.stage_id)
.env("RUSTC", cwd.join(&self.config.rustc_path))
.env("TMPDIR", &tmpdir)
.env("LD_LIB_PATH_ENVVAR", dylib_env_var())
.env("HOST_RPATH_DIR", cwd.join(&self.config.compile_lib_path))
.env("TARGET_RPATH_DIR", cwd.join(&self.config.run_lib_path))
.env("LLVM_COMPONENTS", &self.config.llvm_components)
// We for sure don't want these tests to run in parallel, so make
// sure they don't have access to these vars if we run via `make`
// at the top level
.env_remove("MAKEFLAGS")
.env_remove("MFLAGS")
.env_remove("CARGO_MAKEFLAGS"),
);
if !res.status.success() {
self.fatal_proc_rec("run-make test failed: could not build `rmake.rs` recipe", &res);
}
// Finally, we need to run the recipe binary to build and run the actual tests.
debug!(?recipe_bin);
let mut dylib_env_paths = String::new();
dylib_env_paths.push_str(&env::var(dylib_env_var()).unwrap());
dylib_env_paths.push(':');
dylib_env_paths.push_str(&support_lib_path.parent().unwrap().to_string_lossy());
dylib_env_paths.push(':');
dylib_env_paths.push_str(
&stage_std_path.join("rustlib").join(&self.config.host).join("lib").to_string_lossy(),
);
let mut target_rpath_env_path = String::new();
target_rpath_env_path.push_str(&tmpdir.to_string_lossy());
target_rpath_env_path.push(':');
target_rpath_env_path.push_str(&dylib_env_paths);
let mut cmd = Command::new(&recipe_bin);
cmd.current_dir(&self.testpaths.file)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env("LD_LIB_PATH_ENVVAR", dylib_env_var())
.env("TARGET_RPATH_ENV", &target_rpath_env_path)
.env(dylib_env_var(), &dylib_env_paths)
.env("TARGET", &self.config.target)
.env("PYTHON", &self.config.python)
.env("S", &src_root)
.env("RUST_BUILD_STAGE", &self.config.stage_id)
.env("RUSTC", cwd.join(&self.config.rustc_path))
.env("TMPDIR", &tmpdir)
.env("HOST_RPATH_DIR", cwd.join(&self.config.compile_lib_path))
.env("TARGET_RPATH_DIR", cwd.join(&self.config.run_lib_path))
.env("LLVM_COMPONENTS", &self.config.llvm_components)
// We for sure don't want these tests to run in parallel, so make
// sure they don't have access to these vars if we run via `make`
// at the top level
.env_remove("MAKEFLAGS")
.env_remove("MFLAGS")
.env_remove("CARGO_MAKEFLAGS");
if let Some(ref rustdoc) = self.config.rustdoc_path {
cmd.env("RUSTDOC", cwd.join(rustdoc));
}
if let Some(ref rust_demangler) = self.config.rust_demangler_path {
cmd.env("RUST_DEMANGLER", cwd.join(rust_demangler));
}
if let Some(ref node) = self.config.nodejs {
cmd.env("NODE", node);
}
if let Some(ref linker) = self.config.target_linker {
cmd.env("RUSTC_LINKER", linker);
}
if let Some(ref clang) = self.config.run_clang_based_tests_with {
cmd.env("CLANG", clang);
}
if let Some(ref filecheck) = self.config.llvm_filecheck {
cmd.env("LLVM_FILECHECK", filecheck);
}
if let Some(ref llvm_bin_dir) = self.config.llvm_bin_dir {
cmd.env("LLVM_BIN_DIR", llvm_bin_dir);
}
if let Some(ref remote_test_client) = self.config.remote_test_client {
cmd.env("REMOTE_TEST_CLIENT", remote_test_client);
}
// We don't want RUSTFLAGS set from the outside to interfere with
// compiler flags set in the test cases:
cmd.env_remove("RUSTFLAGS");
// Use dynamic musl for tests because static doesn't allow creating dylibs
if self.config.host.contains("musl") {
cmd.env("RUSTFLAGS", "-Ctarget-feature=-crt-static").env("IS_MUSL_HOST", "1");
}
if self.config.bless {
cmd.env("RUSTC_BLESS_TEST", "--bless");
// Assume this option is active if the environment variable is "defined", with _any_ value.
// As an example, a `Makefile` can use this option by:
//
// ifdef RUSTC_BLESS_TEST
// cp "$(TMPDIR)"/actual_something.ext expected_something.ext
// else
// $(DIFF) expected_something.ext "$(TMPDIR)"/actual_something.ext
// endif
}
if self.config.target.contains("msvc") && self.config.cc != "" {
// We need to pass a path to `lib.exe`, so assume that `cc` is `cl.exe`
// and that `lib.exe` lives next to it.
let lib = Path::new(&self.config.cc).parent().unwrap().join("lib.exe");
// MSYS doesn't like passing flags of the form `/foo` as it thinks it's
// a path and instead passes `C:\msys64\foo`, so convert all
// `/`-arguments to MSVC here to `-` arguments.
let cflags = self
.config
.cflags
.split(' ')
.map(|s| s.replace("/", "-"))
.collect::<Vec<_>>()
.join(" ");
let cxxflags = self
.config
.cxxflags
.split(' ')
.map(|s| s.replace("/", "-"))
.collect::<Vec<_>>()
.join(" ");
cmd.env("IS_MSVC", "1")
.env("IS_WINDOWS", "1")
.env("MSVC_LIB", format!("'{}' -nologo", lib.display()))
.env("CC", format!("'{}' {}", self.config.cc, cflags))
.env("CXX", format!("'{}' {}", &self.config.cxx, cxxflags));
} else {
cmd.env("CC", format!("{} {}", self.config.cc, self.config.cflags))
.env("CXX", format!("{} {}", self.config.cxx, self.config.cxxflags))
.env("AR", &self.config.ar);
if self.config.target.contains("windows") {
cmd.env("IS_WINDOWS", "1");
}
}
let (Output { stdout, stderr, status }, truncated) =
self.read2_abbreviated(cmd.spawn().expect("failed to spawn `rmake`"));
if !status.success() {
let res = ProcRes {
status,
stdout: String::from_utf8_lossy(&stdout).into_owned(),
stderr: String::from_utf8_lossy(&stderr).into_owned(),
truncated,
cmdline: format!("{:?}", cmd),
};
self.fatal_proc_rec("rmake recipe failed to complete", &res);
}
}
fn run_js_doc_test(&self) {
if let Some(nodejs) = &self.config.nodejs {
let out_dir = self.output_base_dir();

View File

@ -0,0 +1,6 @@
[package]
name = "run_make_support"
version = "0.0.0"
edition = "2021"
[dependencies]

View File

@ -0,0 +1,151 @@
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn setup_common_build_cmd() -> Command {
let rustc = env::var("RUSTC").unwrap();
let mut cmd = Command::new(rustc);
cmd.arg("--out-dir")
.arg(env::var("TMPDIR").unwrap())
.arg("-L")
.arg(env::var("TMPDIR").unwrap());
cmd
}
fn handle_failed_output(cmd: &str, output: Output, caller_line_number: u32) -> ! {
eprintln!("command failed at line {caller_line_number}");
eprintln!("{cmd}");
eprintln!("output status: `{}`", output.status);
eprintln!("=== STDOUT ===\n{}\n\n", String::from_utf8(output.stdout).unwrap());
eprintln!("=== STDERR ===\n{}\n\n", String::from_utf8(output.stderr).unwrap());
std::process::exit(1)
}
pub fn rustc() -> RustcInvocationBuilder {
RustcInvocationBuilder::new()
}
pub fn aux_build() -> AuxBuildInvocationBuilder {
AuxBuildInvocationBuilder::new()
}
#[derive(Debug)]
pub struct RustcInvocationBuilder {
cmd: Command,
}
impl RustcInvocationBuilder {
fn new() -> Self {
let cmd = setup_common_build_cmd();
Self { cmd }
}
pub fn arg(&mut self, arg: &str) -> &mut RustcInvocationBuilder {
self.cmd.arg(arg);
self
}
#[track_caller]
pub fn run(&mut self) -> Output {
let caller_location = std::panic::Location::caller();
let caller_line_number = caller_location.line();
let output = self.cmd.output().unwrap();
if !output.status.success() {
handle_failed_output(&format!("{:#?}", self.cmd), output, caller_line_number);
}
output
}
}
#[derive(Debug)]
pub struct AuxBuildInvocationBuilder {
cmd: Command,
}
impl AuxBuildInvocationBuilder {
fn new() -> Self {
let mut cmd = setup_common_build_cmd();
cmd.arg("--crate-type=lib");
Self { cmd }
}
pub fn arg(&mut self, arg: &str) -> &mut AuxBuildInvocationBuilder {
self.cmd.arg(arg);
self
}
#[track_caller]
pub fn run(&mut self) -> Output {
let caller_location = std::panic::Location::caller();
let caller_line_number = caller_location.line();
let output = self.cmd.output().unwrap();
if !output.status.success() {
handle_failed_output(&format!("{:#?}", self.cmd), output, caller_line_number);
}
output
}
}
fn run_common(bin_name: &str) -> (Command, Output) {
let target = env::var("TARGET").unwrap();
let bin_name =
if target.contains("windows") { format!("{}.exe", bin_name) } else { bin_name.to_owned() };
let mut bin_path = PathBuf::new();
bin_path.push(env::var("TMPDIR").unwrap());
bin_path.push(&bin_name);
let ld_lib_path_envvar = env::var("LD_LIB_PATH_ENVVAR").unwrap();
let mut cmd = Command::new(bin_path);
cmd.env(&ld_lib_path_envvar, {
let mut paths = vec![];
paths.push(PathBuf::from(env::var("TMPDIR").unwrap()));
for p in env::split_paths(&env::var("TARGET_RPATH_ENV").unwrap()) {
paths.push(p.to_path_buf());
}
for p in env::split_paths(&env::var(&ld_lib_path_envvar).unwrap()) {
paths.push(p.to_path_buf());
}
env::join_paths(paths.iter()).unwrap()
});
if target.contains("windows") {
let mut paths = vec![];
for p in env::split_paths(&std::env::var("PATH").unwrap_or(String::new())) {
paths.push(p.to_path_buf());
}
paths.push(Path::new(&std::env::var("TARGET_RPATH_DIR").unwrap()).to_path_buf());
cmd.env("PATH", env::join_paths(paths.iter()).unwrap());
}
let output = cmd.output().unwrap();
(cmd, output)
}
/// Run a built binary and make sure it succeeds.
#[track_caller]
pub fn run(bin_name: &str) -> Output {
let caller_location = std::panic::Location::caller();
let caller_line_number = caller_location.line();
let (cmd, output) = run_common(bin_name);
if !output.status.success() {
handle_failed_output(&format!("{:#?}", cmd), output, caller_line_number);
}
output
}
/// Run a built binary and make sure it fails.
#[track_caller]
pub fn run_fail(bin_name: &str) -> Output {
let caller_location = std::panic::Location::caller();
let caller_line_number = caller_location.line();
let (cmd, output) = run_common(bin_name);
if output.status.success() {
handle_failed_output(&format!("{:#?}", cmd), output, caller_line_number);
}
output
}

View File

@ -1,6 +0,0 @@
include ../tools.mk
all:
$(RUSTC) --emit=metadata --crate-type lib stable.rs
$(RUSTC) --emit=metadata --extern stable=$(TMPDIR)/libstable.rmeta main.rs 2>&1 >/dev/null \
| $(CGREP) -e "stable since $$(cat $(S)/src/version)(-[a-zA-Z]+)?"

View File

@ -0,0 +1,27 @@
// ignore-tidy-linelength
extern crate run_make_support;
use std::path::PathBuf;
use run_make_support::{aux_build, rustc};
fn main() {
aux_build()
.arg("--emit=metadata")
.arg("stable.rs")
.run();
let mut stable_path = PathBuf::from(env!("TMPDIR"));
stable_path.push("libstable.rmeta");
let output = rustc()
.arg("--emit=metadata")
.arg("--extern")
.arg(&format!("stable={}", &stable_path.to_string_lossy()))
.arg("main.rs")
.run();
let stderr = String::from_utf8_lossy(&output.stderr);
let version = include_str!(concat!(env!("S"), "/src/version"));
let expected_string = format!("stable since {}", version.trim());
assert!(stderr.contains(&expected_string));
}

View File

@ -1,16 +0,0 @@
# ignore-cross-compile
include ../tools.mk
# Test that if we build `b` against a version of `a` that has one set
# of types, it will not run with a dylib that has a different set of
# types.
# NOTE(eddyb) this test only works with the `legacy` mangling,
# and will probably get removed once `legacy` is gone.
all:
$(RUSTC) a.rs --cfg x -C prefer-dynamic -Z unstable-options -C symbol-mangling-version=legacy
$(RUSTC) b.rs -C prefer-dynamic -Z unstable-options -C symbol-mangling-version=legacy
$(call RUN,b)
$(RUSTC) a.rs --cfg y -C prefer-dynamic -Z unstable-options -C symbol-mangling-version=legacy
$(call FAIL,b)

View File

@ -0,0 +1,45 @@
// ignore-tidy-linelength
extern crate run_make_support;
use run_make_support::{run, run_fail, rustc};
fn main() {
rustc()
.arg("a.rs")
.arg("--cfg")
.arg("x")
.arg("-C")
.arg("prefer-dynamic")
.arg("-Z")
.arg("unstable-options")
.arg("-C")
.arg("symbol-mangling-version=legacy")
.run();
rustc()
.arg("b.rs")
.arg("-C")
.arg("prefer-dynamic")
.arg("-Z")
.arg("unstable-options")
.arg("-C")
.arg("symbol-mangling-version=legacy")
.run();
run("b");
rustc()
.arg("a.rs")
.arg("--cfg")
.arg("y")
.arg("-C")
.arg("prefer-dynamic")
.arg("-Z")
.arg("unstable-options")
.arg("-C")
.arg("symbol-mangling-version=legacy")
.run();
run_fail("b");
}