From 5d91ec8bf2b62accad363cea5db85532ce6333b7 Mon Sep 17 00:00:00 2001 From: wandalen Date: Thu, 24 Oct 2024 10:56:19 +0300 Subject: [PATCH] [make] review --- Cargo.toml | 3 +- m4/tests/integration_test.rs | 152 ++-- make/Cargo.toml | 19 + make/src/config.rs | 128 +++ make/src/error_code.rs | 115 +++ make/src/lib.rs | 226 +++++ make/src/main.rs | 285 +++++++ make/src/parser/lex.rs | 206 +++++ make/src/parser/mod.rs | 82 ++ make/src/parser/parse.rs | 536 ++++++++++++ make/src/parser/preprocessor.rs | 325 +++++++ make/src/rule.rs | 338 ++++++++ make/src/rule/config.rs | 33 + make/src/rule/prerequisite.rs | 35 + make/src/rule/recipe.rs | 77 ++ make/src/rule/recipe/config.rs | 47 ++ make/src/rule/target.rs | 113 +++ make/src/signal_handler.rs | 45 + make/src/special_target.rs | 332 ++++++++ make/tests/integration.rs | 797 ++++++++++++++++++ .../makefiles/arguments/dash_cap_c/makefile | 2 + .../makefiles/arguments/dash_cap_c/works.txt | 1 + .../makefiles/arguments/dash_cap_s/makefile | 9 + make/tests/makefiles/arguments/dash_f.mk | 2 + make/tests/makefiles/arguments/dash_i.mk | 3 + make/tests/makefiles/arguments/dash_k.mk | 9 + make/tests/makefiles/arguments/dash_n.mk | 2 + .../makefiles/arguments/dash_p/with_phony.mk | 4 + make/tests/makefiles/arguments/dash_q/blah.c | 2 + .../makefiles/arguments/dash_q/cc_target.mk | 2 + .../makefiles/arguments/dash_r/with_file.mk | 10 + make/tests/makefiles/arguments/dash_s.mk | 2 + .../tests/makefiles/arguments/dash_t/makefile | 5 + .../tests/makefiles/macros/envs_in_recipes.mk | 5 + .../macros/substitutes_in_recipes.mk | 4 + make/tests/makefiles/parsing/comments.mk | 2 + make/tests/makefiles/parsing/empty.mk | 0 make/tests/makefiles/parsing/include/Makefile | 4 + .../makefiles/parsing/include/variables.mk | 2 + .../parsing/suffixes_with_no_targets.mk | 4 + .../prefixes/force_run/with_dry_run.mk | 2 + .../prefixes/force_run/with_touch/makefile | 2 + .../makefiles/recipes/prefixes/ignore.mk | 3 + .../makefiles/recipes/prefixes/multiple.mk | 3 + .../makefiles/recipes/prefixes/silent.mk | 2 + ...ignores_special_targets_as_first_target.mk | 4 + .../makefiles/special_targets/default.mk | 5 + .../tests/makefiles/special_targets/ignore.mk | 5 + .../special_targets/modifiers/additive.mk | 9 + .../special_targets/modifiers/global.mk | 4 + .../special_targets/phony/phony_basic.mk | 3 + .../precious/basic_precious.mk | 7 + .../precious/empty_precious.mk | 1 + .../special_targets/sccs/basic_sccs.mk | 4 + .../tests/makefiles/special_targets/silent.mk | 4 + .../suffixes/clear_suffixes.mk | 9 + .../suffixes/suffixes_basic.mk | 7 + .../validations/without_prerequisites.mk | 5 + .../validations/without_recipes.mk | 5 + .../target_behavior/async_events/signal.mk | 5 + .../target_behavior/basic_chaining.mk | 5 + .../diamond_chaining_with_touches/makefile | 15 + .../makefile_priority/big_Makefile/Makefile | 2 + .../little_makefile/makefile | 2 + .../makefiles/target_behavior/no_targets.mk | 1 + .../target_behavior/recursive_chaining.mk | 5 + make/tests/parser.rs | 446 ++++++++++ 67 files changed, 4456 insertions(+), 77 deletions(-) create mode 100644 make/Cargo.toml create mode 100644 make/src/config.rs create mode 100644 make/src/error_code.rs create mode 100644 make/src/lib.rs create mode 100644 make/src/main.rs create mode 100644 make/src/parser/lex.rs create mode 100644 make/src/parser/mod.rs create mode 100644 make/src/parser/parse.rs create mode 100644 make/src/parser/preprocessor.rs create mode 100644 make/src/rule.rs create mode 100644 make/src/rule/config.rs create mode 100644 make/src/rule/prerequisite.rs create mode 100644 make/src/rule/recipe.rs create mode 100644 make/src/rule/recipe/config.rs create mode 100644 make/src/rule/target.rs create mode 100644 make/src/signal_handler.rs create mode 100644 make/src/special_target.rs create mode 100644 make/tests/integration.rs create mode 100644 make/tests/makefiles/arguments/dash_cap_c/makefile create mode 100644 make/tests/makefiles/arguments/dash_cap_c/works.txt create mode 100644 make/tests/makefiles/arguments/dash_cap_s/makefile create mode 100644 make/tests/makefiles/arguments/dash_f.mk create mode 100644 make/tests/makefiles/arguments/dash_i.mk create mode 100644 make/tests/makefiles/arguments/dash_k.mk create mode 100644 make/tests/makefiles/arguments/dash_n.mk create mode 100644 make/tests/makefiles/arguments/dash_p/with_phony.mk create mode 100644 make/tests/makefiles/arguments/dash_q/blah.c create mode 100644 make/tests/makefiles/arguments/dash_q/cc_target.mk create mode 100644 make/tests/makefiles/arguments/dash_r/with_file.mk create mode 100644 make/tests/makefiles/arguments/dash_s.mk create mode 100644 make/tests/makefiles/arguments/dash_t/makefile create mode 100644 make/tests/makefiles/macros/envs_in_recipes.mk create mode 100644 make/tests/makefiles/macros/substitutes_in_recipes.mk create mode 100644 make/tests/makefiles/parsing/comments.mk create mode 100644 make/tests/makefiles/parsing/empty.mk create mode 100644 make/tests/makefiles/parsing/include/Makefile create mode 100644 make/tests/makefiles/parsing/include/variables.mk create mode 100644 make/tests/makefiles/parsing/suffixes_with_no_targets.mk create mode 100644 make/tests/makefiles/recipes/prefixes/force_run/with_dry_run.mk create mode 100644 make/tests/makefiles/recipes/prefixes/force_run/with_touch/makefile create mode 100644 make/tests/makefiles/recipes/prefixes/ignore.mk create mode 100644 make/tests/makefiles/recipes/prefixes/multiple.mk create mode 100644 make/tests/makefiles/recipes/prefixes/silent.mk create mode 100644 make/tests/makefiles/special_targets/behavior/ignores_special_targets_as_first_target.mk create mode 100644 make/tests/makefiles/special_targets/default.mk create mode 100644 make/tests/makefiles/special_targets/ignore.mk create mode 100644 make/tests/makefiles/special_targets/modifiers/additive.mk create mode 100644 make/tests/makefiles/special_targets/modifiers/global.mk create mode 100644 make/tests/makefiles/special_targets/phony/phony_basic.mk create mode 100644 make/tests/makefiles/special_targets/precious/basic_precious.mk create mode 100644 make/tests/makefiles/special_targets/precious/empty_precious.mk create mode 100644 make/tests/makefiles/special_targets/sccs/basic_sccs.mk create mode 100644 make/tests/makefiles/special_targets/silent.mk create mode 100644 make/tests/makefiles/special_targets/suffixes/clear_suffixes.mk create mode 100644 make/tests/makefiles/special_targets/suffixes/suffixes_basic.mk create mode 100644 make/tests/makefiles/special_targets/validations/without_prerequisites.mk create mode 100644 make/tests/makefiles/special_targets/validations/without_recipes.mk create mode 100644 make/tests/makefiles/target_behavior/async_events/signal.mk create mode 100644 make/tests/makefiles/target_behavior/basic_chaining.mk create mode 100644 make/tests/makefiles/target_behavior/diamond_chaining_with_touches/makefile create mode 100644 make/tests/makefiles/target_behavior/makefile_priority/big_Makefile/Makefile create mode 100644 make/tests/makefiles/target_behavior/makefile_priority/little_makefile/makefile create mode 100644 make/tests/makefiles/target_behavior/no_targets.mk create mode 100644 make/tests/makefiles/target_behavior/recursive_chaining.mk create mode 100644 make/tests/parser.rs diff --git a/Cargo.toml b/Cargo.toml index 8697a0a2..3bf94c99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ members = [ "display", "file", "fs", - "ftw", + "ftw", + "make", "m4", "m4/test-manager", "gettext-rs", diff --git a/m4/tests/integration_test.rs b/m4/tests/integration_test.rs index e3497ec4..8d0e51c8 100644 --- a/m4/tests/integration_test.rs +++ b/m4/tests/integration_test.rs @@ -109,7 +109,7 @@ fn run_command(input: &Path) -> std::process::Output { #[test] fn test_bsd() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/bsd.m4")); + let output = run_command(Path::new("fixtures/integration_tests/bsd.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/bsd.out"); assert_eq!( @@ -135,7 +135,7 @@ fn test_bsd() { #[test] fn test_bsd_math() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/bsd_math.m4")); + let output = run_command(Path::new("fixtures/integration_tests/bsd_math.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/bsd_math.out"); assert_eq!( @@ -160,7 +160,7 @@ fn test_bsd_math() { #[test] fn test_changecom() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/changecom.m4")); + let output = run_command(Path::new("fixtures/integration_tests/changecom.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/changecom.out"); assert_eq!( @@ -185,7 +185,7 @@ fn test_changecom() { #[test] fn test_changequote() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/changequote.m4")); + let output = run_command(Path::new("fixtures/integration_tests/changequote.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/changequote.out"); assert_eq!( @@ -210,7 +210,7 @@ fn test_changequote() { #[test] fn test_decr() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/decr.m4")); + let output = run_command(Path::new("fixtures/integration_tests/decr.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/decr.out"); assert_eq!( @@ -235,7 +235,7 @@ fn test_decr() { #[test] fn test_define() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/define.m4")); + let output = run_command(Path::new("fixtures/integration_tests/define.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/define.out"); assert_eq!( @@ -260,7 +260,7 @@ fn test_define() { #[test] fn test_define_args() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/define_args.args")); + let output = run_command(Path::new("fixtures/integration_tests/define_args.args")); let test: TestSnapshot = read_test("fixtures/integration_tests/define_args.out"); assert_eq!( @@ -286,7 +286,7 @@ fn test_define_args() { #[test] fn test_define_eval_order_unquoted() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_eval_order_unquoted.m4", )); @@ -314,7 +314,7 @@ fn test_define_eval_order_unquoted() { #[test] fn test_define_eval_syntax_order_quoted_evaluated() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_eval_syntax_order_quoted_evaluated.m4", )); @@ -342,7 +342,7 @@ fn test_define_eval_syntax_order_quoted_evaluated() { #[test] fn test_define_eval_syntax_order_quoted_unevaluated() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_eval_syntax_order_quoted_unevaluated.m4", )); @@ -371,7 +371,7 @@ fn test_define_eval_syntax_order_quoted_unevaluated() { #[test] fn test_define_eval_syntax_order_unquoted() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_eval_syntax_order_unquoted.m4", )); @@ -399,7 +399,7 @@ fn test_define_eval_syntax_order_unquoted() { #[test] fn test_define_hanging_quotes() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_hanging_quotes.m4", )); @@ -426,7 +426,7 @@ fn test_define_hanging_quotes() { #[test] fn test_define_invalid_macro_name() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_invalid_macro_name.m4", )); @@ -453,7 +453,7 @@ fn test_define_invalid_macro_name() { #[test] fn test_define_iterative() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/define_iterative.m4")); + let output = run_command(Path::new("fixtures/integration_tests/define_iterative.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/define_iterative.out"); assert_eq!( @@ -478,7 +478,7 @@ fn test_define_iterative() { #[test] fn test_define_iterative_2() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_iterative_2.m4", )); @@ -505,7 +505,7 @@ fn test_define_iterative_2() { #[test] fn test_define_nested() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/define_nested.m4")); + let output = run_command(Path::new("fixtures/integration_tests/define_nested.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/define_nested.out"); assert_eq!( @@ -531,7 +531,7 @@ fn test_define_nested() { #[test] fn test_define_nested_first_arg() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_nested_first_arg.m4", )); @@ -558,7 +558,7 @@ fn test_define_nested_first_arg() { #[test] fn test_define_number_parsing() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_number_parsing.m4", )); @@ -583,7 +583,7 @@ fn test_define_number_parsing() { #[test] fn test_define_order_defined() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_order_defined.m4", )); @@ -610,7 +610,7 @@ fn test_define_order_defined() { #[test] fn test_define_order_undefined() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_order_undefined.m4", )); @@ -637,7 +637,7 @@ fn test_define_order_undefined() { #[test] fn test_define_parse_brackets() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_parse_brackets.m4", )); @@ -662,7 +662,7 @@ fn test_define_parse_brackets() { #[test] fn test_define_pushpopdef_undefine() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_pushpopdef_undefine.m4", )); @@ -689,7 +689,7 @@ fn test_define_pushpopdef_undefine() { #[test] fn test_define_quoted_number_stacked() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_quoted_number_stacked.m4", )); @@ -715,7 +715,7 @@ fn test_define_quoted_number_stacked() { #[test] fn test_define_stacked() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/define_stacked.m4")); + let output = run_command(Path::new("fixtures/integration_tests/define_stacked.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/define_stacked.out"); assert_eq!( @@ -740,7 +740,7 @@ fn test_define_stacked() { #[test] fn test_define_undefine_order() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_undefine_order.m4", )); @@ -767,7 +767,7 @@ fn test_define_undefine_order() { #[test] fn test_define_unquoted_number_arg() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/define_unquoted_number_arg.m4", )); @@ -792,7 +792,7 @@ fn test_define_unquoted_number_arg() { #[test] fn test_defn() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/defn.m4")); + let output = run_command(Path::new("fixtures/integration_tests/defn.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/defn.out"); assert_eq!( @@ -817,7 +817,7 @@ fn test_defn() { #[test] fn test_divert() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/divert.m4")); + let output = run_command(Path::new("fixtures/integration_tests/divert.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/divert.out"); assert_eq!( @@ -842,7 +842,7 @@ fn test_divert() { #[test] fn test_divert_nested() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/divert_nested.m4")); + let output = run_command(Path::new("fixtures/integration_tests/divert_nested.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/divert_nested.out"); assert_eq!( @@ -867,7 +867,7 @@ fn test_divert_nested() { #[test] fn test_divert_nested_2() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/divert_nested_2.m4")); + let output = run_command(Path::new("fixtures/integration_tests/divert_nested_2.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/divert_nested_2.out"); assert_eq!( @@ -892,7 +892,7 @@ fn test_divert_nested_2() { #[test] fn test_divert_nested_3() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/divert_nested_3.m4")); + let output = run_command(Path::new("fixtures/integration_tests/divert_nested_3.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/divert_nested_3.out"); assert_eq!( @@ -917,7 +917,7 @@ fn test_divert_nested_3() { #[test] fn test_divert_nested_4() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/divert_nested_4.m4")); + let output = run_command(Path::new("fixtures/integration_tests/divert_nested_4.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/divert_nested_4.out"); assert_eq!( @@ -942,7 +942,7 @@ fn test_divert_nested_4() { #[test] fn test_dnl() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/dnl.m4")); + let output = run_command(Path::new("fixtures/integration_tests/dnl.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/dnl.out"); assert_eq!( @@ -967,7 +967,7 @@ fn test_dnl() { #[test] fn test_dnl_nested() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/dnl_nested.m4")); + let output = run_command(Path::new("fixtures/integration_tests/dnl_nested.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/dnl_nested.out"); assert_eq!( @@ -992,7 +992,7 @@ fn test_dnl_nested() { #[test] fn test_dumpdef() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/dumpdef.m4")); + let output = run_command(Path::new("fixtures/integration_tests/dumpdef.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/dumpdef.out"); assert_eq!( @@ -1017,7 +1017,7 @@ fn test_dumpdef() { #[test] fn test_dumpdef_notexist() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/dumpdef_notexist.m4")); + let output = run_command(Path::new("fixtures/integration_tests/dumpdef_notexist.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/dumpdef_notexist.out"); assert_eq!( @@ -1040,7 +1040,7 @@ fn test_dumpdef_notexist() { #[test] fn test_eval() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/eval.m4")); + let output = run_command(Path::new("fixtures/integration_tests/eval.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/eval.out"); assert_eq!( @@ -1065,7 +1065,7 @@ fn test_eval() { #[test] fn test_evaluation_order() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/evaluation_order.m4")); + let output = run_command(Path::new("fixtures/integration_tests/evaluation_order.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/evaluation_order.out"); assert_eq!( @@ -1090,7 +1090,7 @@ fn test_evaluation_order() { #[test] fn test_file() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/file.m4")); + let output = run_command(Path::new("fixtures/integration_tests/file.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/file.out"); assert_eq!( @@ -1115,7 +1115,7 @@ fn test_file() { #[test] fn test_forloop_nested() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/forloop_nested.m4")); + let output = run_command(Path::new("fixtures/integration_tests/forloop_nested.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/forloop_nested.out"); assert_eq!( @@ -1140,7 +1140,7 @@ fn test_forloop_nested() { #[test] fn test_forloop_simple() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/forloop_simple.m4")); + let output = run_command(Path::new("fixtures/integration_tests/forloop_simple.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/forloop_simple.out"); assert_eq!( @@ -1165,7 +1165,7 @@ fn test_forloop_simple() { #[test] fn test_ifdef() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/ifdef.m4")); + let output = run_command(Path::new("fixtures/integration_tests/ifdef.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/ifdef.out"); assert_eq!( @@ -1190,7 +1190,7 @@ fn test_ifdef() { #[test] fn test_ifelse() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/ifelse.m4")); + let output = run_command(Path::new("fixtures/integration_tests/ifelse.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/ifelse.out"); assert_eq!( @@ -1215,7 +1215,7 @@ fn test_ifelse() { #[test] fn test_include() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/include.m4")); + let output = run_command(Path::new("fixtures/integration_tests/include.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/include.out"); assert_eq!( @@ -1240,7 +1240,7 @@ fn test_include() { #[test] fn test_include_divert() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/include_divert.m4")); + let output = run_command(Path::new("fixtures/integration_tests/include_divert.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/include_divert.out"); assert_eq!( @@ -1265,7 +1265,7 @@ fn test_include_divert() { #[test] fn test_incr() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/incr.m4")); + let output = run_command(Path::new("fixtures/integration_tests/incr.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/incr.out"); assert_eq!( @@ -1290,7 +1290,7 @@ fn test_incr() { #[test] fn test_index() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/index.m4")); + let output = run_command(Path::new("fixtures/integration_tests/index.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/index.out"); assert_eq!( @@ -1315,7 +1315,7 @@ fn test_index() { #[test] fn test_index_too_few_args() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/index_too_few_args.m4", )); @@ -1340,7 +1340,7 @@ fn test_index_too_few_args() { #[test] fn test_len() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/len.m4")); + let output = run_command(Path::new("fixtures/integration_tests/len.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/len.out"); assert_eq!( @@ -1365,7 +1365,7 @@ fn test_len() { #[test] fn test_m4exit_error() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/m4exit_error.m4")); + let output = run_command(Path::new("fixtures/integration_tests/m4exit_error.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/m4exit_error.out"); assert_eq!( @@ -1390,7 +1390,7 @@ fn test_m4exit_error() { #[test] fn test_m4exit_no_args() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/m4exit_no_args.m4")); + let output = run_command(Path::new("fixtures/integration_tests/m4exit_no_args.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/m4exit_no_args.out"); assert_eq!( @@ -1415,7 +1415,7 @@ fn test_m4exit_no_args() { #[test] fn test_m4exit_success() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/m4exit_success.m4")); + let output = run_command(Path::new("fixtures/integration_tests/m4exit_success.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/m4exit_success.out"); assert_eq!( @@ -1440,7 +1440,7 @@ fn test_m4exit_success() { #[test] fn test_m4wrap() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/m4wrap.m4")); + let output = run_command(Path::new("fixtures/integration_tests/m4wrap.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/m4wrap.out"); assert_eq!( @@ -1465,7 +1465,7 @@ fn test_m4wrap() { #[test] fn test_macro_errprint_evaluation() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/macro_errprint_evaluation.m4", )); @@ -1492,7 +1492,7 @@ fn test_macro_errprint_evaluation() { #[test] fn test_macro_errprint_no_evaluation() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/macro_errprint_no_evaluation.m4", )); @@ -1520,7 +1520,7 @@ fn test_macro_errprint_no_evaluation() { #[test] fn test_macro_errprint_no_evaluation_quoted() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/macro_errprint_no_evaluation_quoted.m4", )); @@ -1548,7 +1548,7 @@ fn test_macro_errprint_no_evaluation_quoted() { #[test] fn test_maketemp() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/maketemp.m4")); + let output = run_command(Path::new("fixtures/integration_tests/maketemp.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/maketemp.out"); assert_eq!( @@ -1574,7 +1574,7 @@ fn test_maketemp() { #[test] fn test_mkstemp() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/mkstemp.m4")); + let output = run_command(Path::new("fixtures/integration_tests/mkstemp.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/mkstemp.out"); assert_eq!( @@ -1600,7 +1600,7 @@ fn test_mkstemp() { #[test] fn test_quoted_nested_eof_in_string() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/quoted_nested_eof_in_string.m4", )); @@ -1626,7 +1626,7 @@ fn test_quoted_nested_eof_in_string() { #[test] fn test_recurse() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/recurse.m4")); + let output = run_command(Path::new("fixtures/integration_tests/recurse.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/recurse.out"); assert_eq!( @@ -1651,7 +1651,7 @@ fn test_recurse() { #[test] fn test_recursive_defines() { init(); - let output = run_command(&Path::new( + let output = run_command(Path::new( "fixtures/integration_tests/recursive_defines.m4", )); @@ -1678,7 +1678,7 @@ fn test_recursive_defines() { #[test] fn test_redefine_inbuilt() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/redefine_inbuilt.m4")); + let output = run_command(Path::new("fixtures/integration_tests/redefine_inbuilt.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/redefine_inbuilt.out"); assert_eq!( @@ -1703,7 +1703,7 @@ fn test_redefine_inbuilt() { #[test] fn test_reverse() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/reverse.m4")); + let output = run_command(Path::new("fixtures/integration_tests/reverse.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/reverse.out"); assert_eq!( @@ -1728,7 +1728,7 @@ fn test_reverse() { #[test] fn test_shift() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/shift.m4")); + let output = run_command(Path::new("fixtures/integration_tests/shift.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/shift.out"); assert_eq!( @@ -1753,7 +1753,7 @@ fn test_shift() { #[test] fn test_sinclude() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/sinclude.m4")); + let output = run_command(Path::new("fixtures/integration_tests/sinclude.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/sinclude.out"); assert_eq!( @@ -1778,7 +1778,7 @@ fn test_sinclude() { #[test] fn test_substr() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/substr.m4")); + let output = run_command(Path::new("fixtures/integration_tests/substr.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/substr.out"); assert_eq!( @@ -1804,7 +1804,7 @@ fn test_substr() { #[test] fn test_synclines_1() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/synclines_1.args")); + let output = run_command(Path::new("fixtures/integration_tests/synclines_1.args")); let test: TestSnapshot = read_test("fixtures/integration_tests/synclines_1.out"); assert_eq!( @@ -1830,7 +1830,7 @@ fn test_synclines_1() { #[test] fn test_synclines_2() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/synclines_2.args")); + let output = run_command(Path::new("fixtures/integration_tests/synclines_2.args")); let test: TestSnapshot = read_test("fixtures/integration_tests/synclines_2.out"); assert_eq!( @@ -1856,7 +1856,7 @@ fn test_synclines_2() { #[test] fn test_syscmd_sysval() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/syscmd_sysval.m4")); + let output = run_command(Path::new("fixtures/integration_tests/syscmd_sysval.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/syscmd_sysval.out"); assert_eq!( @@ -1881,7 +1881,7 @@ fn test_syscmd_sysval() { #[test] fn test_trace() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/trace.m4")); + let output = run_command(Path::new("fixtures/integration_tests/trace.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/trace.out"); assert_eq!( @@ -1906,7 +1906,7 @@ fn test_trace() { #[test] fn test_translit() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/translit.m4")); + let output = run_command(Path::new("fixtures/integration_tests/translit.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/translit.out"); assert_eq!( @@ -1931,7 +1931,7 @@ fn test_translit() { #[test] fn test_two_files() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/two_files.args")); + let output = run_command(Path::new("fixtures/integration_tests/two_files.args")); let test: TestSnapshot = read_test("fixtures/integration_tests/two_files.out"); assert_eq!( @@ -1956,7 +1956,7 @@ fn test_two_files() { #[test] fn test_undivert() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/undivert.m4")); + let output = run_command(Path::new("fixtures/integration_tests/undivert.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/undivert.out"); assert_eq!( @@ -1981,7 +1981,7 @@ fn test_undivert() { #[test] fn test_undivert_2() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/undivert_2.m4")); + let output = run_command(Path::new("fixtures/integration_tests/undivert_2.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/undivert_2.out"); assert_eq!( @@ -2006,7 +2006,7 @@ fn test_undivert_2() { #[test] fn test_undivert_current() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/undivert_current.m4")); + let output = run_command(Path::new("fixtures/integration_tests/undivert_current.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/undivert_current.out"); assert_eq!( @@ -2031,7 +2031,7 @@ fn test_undivert_current() { #[test] fn test_undivert_nested() { init(); - let output = run_command(&Path::new("fixtures/integration_tests/undivert_nested.m4")); + let output = run_command(Path::new("fixtures/integration_tests/undivert_nested.m4")); let test: TestSnapshot = read_test("fixtures/integration_tests/undivert_nested.out"); assert_eq!( diff --git a/make/Cargo.toml b/make/Cargo.toml new file mode 100644 index 00000000..3b3d5368 --- /dev/null +++ b/make/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "posixutils-make" +version = "0.1.0" +edition = "2021" +authors = ["Jeff Garzik"] +license = "MIT" +repository = "https://github.com/rustcoreutils/posixutils-rs.git" + +[dependencies] +plib = { path = "../plib" } +clap.workspace = true +libc.workspace = true +gettext-rs.workspace = true +const_format = "0.2" +rowan = "0.15" + +[[bin]] +name = "make" +path = "src/main.rs" diff --git a/make/src/config.rs b/make/src/config.rs new file mode 100644 index 00000000..1ce5f1b8 --- /dev/null +++ b/make/src/config.rs @@ -0,0 +1,128 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use std::collections::{BTreeMap, BTreeSet}; + +/// Represents the configuration of the make utility +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Config { + /// Whether to ignore the errors in the rule + pub ignore: bool, + /// Whether to execute commands or print to stdout + pub dry_run: bool, + /// Whether to print recipe lines + pub silent: bool, + /// Whether to touch targets on execution + pub touch: bool, + /// Whether to replace macros within makefiles with envs + pub env_macros: bool, + /// Whether to quit without build + pub quit: bool, + /// Whether to keep going build targets and write info about errors stderr + pub keep_going: bool, + /// Whether to terminate on error + pub terminate: bool, + /// Whether to clear default_rules + pub clear: bool, + /// Whether to print macro definitions and target descriptions. + pub print: bool, + /// Whether to not delete interrupted files on async events. + pub precious: bool, + + pub rules: BTreeMap>, +} + +impl Default for Config { + fn default() -> Self { + Self { + ignore: false, + dry_run: false, + silent: false, + touch: false, + env_macros: false, + keep_going: false, + quit: false, + clear: false, + print: false, + precious: false, + terminate: true, + rules: BTreeMap::from([ + ( + ".SUFFIXES".to_string(), + vec![ + ".o", ".c", ".y", ".l", ".a", ".sh", ".c~", ".y~", ".l~", ".sh~", + ] + .into_iter() + .map(String::from) + .collect(), + ), + ( + ".SCCS_GET".to_string(), + BTreeSet::from([String::from("sccs $(SCCSFLAGS) get $(SCCSGETFLAGS) $@")]), + ), + ( + ".MACROS".to_string(), + vec![ + "AR=ar", + "ARFLAGS=-rv", + "YACC=yacc", + "YFLAGS=", + "LEX=lex", + "LFLAGS=", + "LDFLAGS=", + "CC=c17", + "CFLAGS=-O 1", + "XSI GET=get", + "GFLAGS=", + "SCCSFLAGS=", + "SCCSGETFLAGS=-s", + ] + .into_iter() + .map(String::from) + .collect(), + ), + ( + "SUFFIX RULES".to_string(), + [ + // Single-Suffix Rules + ".c: $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<", + ".sh: cp $< $@", + ".sh: chmod a+x $@", + + // Double-Suffix Rules + ".c.o: $(CC) $(CFLAGS) -c $<", + ".y.o: $(YACC) $(YFLAGS) $<; $(CC) $(CFLAGS) -c y.tab.c; rm -f y.tab.c; mv y.tab.o $@", + ".l.o: $(LEX) $(LFLAGS) $<; $(CC) $(CFLAGS) -c lex.yy.c; rm -f lex.yy.c; mv lex.yy.o $@", + ".y.c: $(YACC) $(YFLAGS) $<; mv y.tab.c $@", + ".l.c: $(LEX) $(LFLAGS) $<; mv lex.yy.c $@", + "XSI .c~.o: $(GET) $(GFLAGS) -p $< > $*.c; $(CC) $(CFLAGS) -c $*.c", + ".y~.o: $(GET) $(GFLAGS) -p $< > $*.y; $(YACC) $(YFLAGS) $*.y; $(CC) $(CFLAGS) -c y.tab.c; rm -f y.tab.c; mv y.tab.o $@", + ".l~.o: $(GET) $(GFLAGS) -p $< > $*.l; $(LEX) $(LFLAGS) $*.l; $(CC) $(CFLAGS) -c lex.yy.c; rm -f lex.yy.c; mv lex.yy.o $@", + ".y~.c: $(GET) $(GFLAGS) -p $< > $*.y; $(YACC) $(YFLAGS) $*.y; mv y.tab.c $@", + ".l~.c: $(GET) $(GFLAGS) -p $< > $*.l; $(LEX) $(LFLAGS) $*.l; mv lex.yy.c $@", + ".c.a: $(CC) -c $(CFLAGS) $<; $(AR) $(ARFLAGS) $@ $*.o; rm -f $*.o", + ] + .into_iter() + .map(String::from) + .collect::>(), + ) + ]), + } + } +} + +impl Config { + /// Adds a new suffix to the `.SUFFIXES` rule. + pub fn add_suffix(&mut self, new_suffix: &str) { + self.rules + .entry(".SUFFIXES".to_string()) + .or_default() + .insert(new_suffix.to_string()); + } +} diff --git a/make/src/error_code.rs b/make/src/error_code.rs new file mode 100644 index 00000000..23f642f6 --- /dev/null +++ b/make/src/error_code.rs @@ -0,0 +1,115 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use core::fmt; +use std::io; + +use crate::parser::parse::ParseError; +use crate::special_target::Error; +use gettextrs::gettext; + +/// Represents the error codes that can be returned by the make utility +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ErrorCode { + // Transparent + ExecutionError { exit_code: Option }, + IoError(io::ErrorKind), + ParserError { constraint: ParseError }, + + // Specific + NoMakefile, + NotUpToDateError { target: String }, + NoTarget { target: Option }, + NoRule { rule: String }, + RecursivePrerequisite { origin: String }, + SpecialTargetConstraintNotFulfilled { target: String, constraint: Error }, +} + +impl From for i32 { + fn from(err: ErrorCode) -> i32 { + (&err).into() + } +} + +// todo: tests error codes +impl From<&ErrorCode> for i32 { + fn from(err: &ErrorCode) -> i32 { + use ErrorCode::*; + + match err { + NotUpToDateError { .. } => 1, + ExecutionError { .. } => 2, + IoError(_) => 3, + ParserError { .. } => 4, + NoMakefile => 5, + NoTarget { .. } => 6, + NoRule { .. } => 7, + RecursivePrerequisite { .. } => 8, + SpecialTargetConstraintNotFulfilled { .. } => 9, + } + } +} + +impl fmt::Display for ErrorCode { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use ErrorCode::*; + + match self { + NotUpToDateError { target } => { + write!(f, "{}: {}", target, gettext("target is not up to date")) + } + ExecutionError { exit_code } => match exit_code { + Some(exit_code) => { + write!(f, "{}: {}", gettext("execution error"), exit_code) + } + None => { + write!( + f, + "{}: {}", + gettext("execution error"), + gettext("terminated by signal"), + ) + } + }, + IoError(err) => write!(f, "{}: {}", gettext("io error"), err), + NoMakefile => write!(f, "{}", gettext("no makefile")), + ParserError { constraint } => write!(f, "{}: {}", gettext("parse error"), constraint), + NoTarget { target } => match target { + Some(target) => write!(f, "{} '{}'", gettext("no target"), target), + None => write!(f, "{}", gettext("no targets to execute")), + }, + NoRule { rule } => write!(f, "{} '{}'", gettext("no rule"), rule), + RecursivePrerequisite { origin } => { + write!( + f, + "{} '{}'", + gettext("recursive prerequisite found trying to build"), + origin, + ) + } + SpecialTargetConstraintNotFulfilled { target, constraint } => { + write!( + f, + "'{}' {}: {}", + target, + gettext("special target constraint is not fulfilled"), + constraint, + ) + } + } + } +} + +impl std::error::Error for ErrorCode {} + +impl From for ErrorCode { + fn from(err: io::Error) -> Self { + Self::IoError(err.kind()) + } +} diff --git a/make/src/lib.rs b/make/src/lib.rs new file mode 100644 index 00000000..74abb027 --- /dev/null +++ b/make/src/lib.rs @@ -0,0 +1,226 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +pub mod config; +pub mod error_code; +pub mod parser; +pub mod rule; +pub mod signal_handler; +pub mod special_target; + +use std::{ + collections::HashSet, + fs::{self}, + time::SystemTime, +}; + +use parser::{Makefile, VariableDefinition}; + +use crate::special_target::InferenceTarget; +use config::Config; +use error_code::ErrorCode::{self, *}; +use rule::{prerequisite::Prerequisite, target::Target, Rule}; +use special_target::SpecialTarget; + +/// The default shell variable name. +const DEFAULT_SHELL_VAR: &str = "SHELL"; + +/// The default shell to use for running recipes. Linux and MacOS +const DEFAULT_SHELL: &str = "/bin/sh"; + +/// Represents the make utility with its data and configuration. +/// +/// The only way to create a Make is from a Makefile and a Config. +pub struct Make { + macros: Vec, + rules: Vec, + default_rule: Option, // .DEFAULT + pub config: Config, +} + +impl Make { + /// Retrieves the rule that has the given target. + /// + /// # Returns + /// + /// - Some(rule) if a rule with the target exists. + /// - None if no rule with the target exists. + fn rule_by_target_name(&self, target: impl AsRef) -> Option<&Rule> { + self.rules + .iter() + .find(|rule| rule.targets().any(|t| t.as_ref() == target.as_ref())) + } + + pub fn first_target(&self) -> Result<&Target, ErrorCode> { + let rule = self.rules.first().ok_or(NoTarget { target: None })?; + rule.targets().next().ok_or(NoTarget { target: None }) + } + + /// Builds the target with the given name. + /// + /// # Returns + /// - Ok(true) if the target was built. + /// - Ok(false) if the target was already up to date. + /// - Err(_) if any errors occur. + pub fn build_target(&self, name: impl AsRef) -> Result { + let rule = match self.rule_by_target_name(&name) { + Some(rule) => rule, + None => match &self.default_rule { + Some(rule) => rule, + None => { + return Err(NoTarget { + target: Some(name.as_ref().to_string()), + }) + } + }, + }; + let target = Target::new(name.as_ref()); + + self.run_rule_with_prerequisites(rule, &target) + } + + /// Runs the given rule. + /// + /// # Returns + /// - Ok(true) if the rule was run. + /// - Ok(false) if the rule was already up to date. + /// - Err(_) if any errors occur. + fn run_rule_with_prerequisites(&self, rule: &Rule, target: &Target) -> Result { + if self.are_prerequisites_recursive(target) { + return Err(RecursivePrerequisite { + origin: target.to_string(), + }); + } + + let newer_prerequisites = self.get_newer_prerequisites(target); + let mut up_to_date = newer_prerequisites.is_empty() && get_modified_time(target).is_some(); + if rule.config.phony { + up_to_date = false; + } + + if up_to_date { + return Ok(false); + } + + for prerequisite in &newer_prerequisites { + self.build_target(prerequisite)?; + } + rule.run(&self.config, &self.macros, target, up_to_date)?; + + Ok(true) + } + + /// Retrieves the prerequisites of the target that are newer than the target. + /// Recursively checks the prerequisites of the prerequisites. + /// Returns an empty vector if the target does not exist (or it's a file). + fn get_newer_prerequisites(&self, target: impl AsRef) -> Vec<&Prerequisite> { + let Some(target_rule) = self.rule_by_target_name(&target) else { + return vec![]; + }; + let target_modified = get_modified_time(target); + + let prerequisites = target_rule.prerequisites(); + + if let Some(target_modified) = target_modified { + prerequisites + .filter(|prerequisite| { + let Some(pre_modified) = get_modified_time(prerequisite) else { + return true; + }; + + !self.get_newer_prerequisites(prerequisite).is_empty() + || pre_modified > target_modified + }) + .collect() + } else { + prerequisites.collect() + } + } + + /// Checks if the target has recursive prerequisites. + /// Returns true if the target has recursive prerequisites. + fn are_prerequisites_recursive(&self, target: impl AsRef) -> bool { + let mut visited = HashSet::from([target.as_ref()]); + let mut stack = HashSet::from([target.as_ref()]); + + self._are_prerequisites_recursive(target.as_ref(), &mut visited, &mut stack) + } + + /// A helper function to check if the target has recursive prerequisites. + /// Uses DFS to check for recursive prerequisites. + fn _are_prerequisites_recursive( + &self, + target: &str, + visited: &mut HashSet<&str>, + stack: &mut HashSet<&str>, + ) -> bool { + let Some(rule) = self.rule_by_target_name(target) else { + return false; + }; + + let prerequisites = rule.prerequisites(); + + for prerequisite in prerequisites { + if (!visited.contains(prerequisite.as_ref()) + && self._are_prerequisites_recursive(prerequisite.as_ref(), visited, stack)) + || stack.contains(prerequisite.as_ref()) + { + return true; + } + } + + stack.remove(target); + false + } +} + +impl TryFrom<(Makefile, Config)> for Make { + type Error = ErrorCode; + + fn try_from((makefile, config): (Makefile, Config)) -> Result { + let mut rules = vec![]; + let mut special_rules = vec![]; + let mut inference_rules = vec![]; + + for rule in makefile.rules() { + let rule = Rule::from(rule); + let Some(target) = rule.targets().next() else { + return Err(NoTarget { target: None }); + }; + + if SpecialTarget::try_from(target.clone()).is_ok() { + special_rules.push(rule); + } else if InferenceTarget::try_from((target.clone(), config.clone())).is_ok() { + inference_rules.push(rule); + } else { + rules.push(rule); + } + } + + let mut make = Self { + rules, + macros: makefile.variable_definitions().collect(), + default_rule: None, + config, + }; + + for rule in special_rules { + special_target::process(rule, &mut make)?; + } + + Ok(make) + } +} + +/// Retrieves the modified time of the file at the given path. +fn get_modified_time(path: impl AsRef) -> Option { + fs::metadata(path.as_ref()) + .ok() + .and_then(|meta| meta.modified().ok()) +} diff --git a/make/src/main.rs b/make/src/main.rs new file mode 100644 index 00000000..7b81e71a --- /dev/null +++ b/make/src/main.rs @@ -0,0 +1,285 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use clap::Parser; +use const_format::formatcp; +use core::str::FromStr; +use gettextrs::{bind_textdomain_codeset, gettext, textdomain}; +use plib::PROJECT_NAME; +use posixutils_make::{ + config::Config, + error_code::ErrorCode::{self, *}, + parser::{preprocessor::ENV_MACROS, Makefile}, + Make, +}; +use std::sync::atomic::Ordering::Relaxed; +use std::{ + collections::{BTreeMap, BTreeSet}, + env, + ffi::OsString, + fs, + io::{self, Read}, + path::{Path, PathBuf}, + process, +}; + +const MAKEFILE_NAME: [&str; 2] = ["makefile", "Makefile"]; +const MAKEFILE_PATH: [&str; 2] = [ + formatcp!("./{}", MAKEFILE_NAME[0]), + formatcp!("./{}", MAKEFILE_NAME[1]), +]; + +// todo: sort arguments +#[derive(Parser, Debug)] +struct Args { + #[arg(short = 'C', long, help = "Change to DIRECTORY before doing anything")] + directory: Option, + + #[arg( + short = 'S', + long, + help = "Terminate make if error occurs. Default behavior" + )] + terminate: bool, + + #[arg(short = 'f', long, help = "Path to the makefile to parse")] + makefile: Option, + + #[arg(short = 'i', long, help = "Ignore errors in the recipe")] + ignore: bool, + + #[arg( + short = 'e', + long, + help = "Cause environment variables to override macro assignments within makefiles" + )] + env_macros: bool, + + #[arg( + short = 'n', + long, + help = "Print commands to stdout and do not execute them" + )] + dry_run: bool, + + #[arg(short = 's', long, help = "Do not print recipe lines")] + silent: bool, + + #[arg( + short = 'q', + long, + help = "Return a zero exit value if the target file is up-to-date; otherwise, return an exit value of 1." + )] + quit: bool, + + #[arg( + short = 'k', + long, + help = "Continue to update other targets that do not depend on the current target if a non-ignored error occur" + )] + keep_going: bool, + + #[arg( + short = 'r', + long, + help = "Clear the suffix list and do not use the built-in rules" + )] + clear: bool, + + #[arg( + short = 'p', + long, + help = "Write to standard output the complete set of macro definitions and target descriptions." + )] + print: bool, + + #[arg( + short = 't', + long, + help = "If makefile should touch targets on execution" + )] + touch: bool, + + #[arg(help = "Targets to build")] + targets: Vec, +} + +fn main() -> Result<(), Box> { + textdomain(PROJECT_NAME)?; + bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; + + let Args { + directory, + makefile, + env_macros, + ignore, + dry_run, + silent, + quit, + clear, + touch, + print, + terminate, + keep_going, + mut targets, + } = Args::parse(); + + let mut status_code = 0; + + // -C flag + if let Some(dir) = directory { + env::set_current_dir(dir)?; + } + + let mut config = Config { + ignore, + dry_run, + silent, + touch, + env_macros, + quit, + keep_going, + clear, + print, + precious: false, + terminate, + ..Default::default() + }; + + if clear { + config.rules.clear(); + } + + ENV_MACROS.store(env_macros, Relaxed); + + let parsed = match parse_makefile(makefile.as_ref()) { + Ok(parsed) => parsed, + Err(err) => { + // -p flag + if print { + // If makefile is not provided or parsing failed, print the default rules + print_rules(&config.rules); + return Ok(()); + } else { + eprintln!("make: {}", err); + process::exit(err.into()); + } + } + }; + + let make = Make::try_from((parsed, config)).unwrap_or_else(|err| { + eprintln!("make: {err}"); + process::exit(err.into()); + }); + + // -p flag + if print { + // Call print for global config rules + print_rules(&make.config.rules); + } + + if targets.is_empty() { + let target = make + .first_target() + .unwrap_or_else(|err| { + eprintln!("make: {err}"); + process::exit(err.into()); + }) + .to_string() + .into(); + + targets.push(target); + } + + let mut had_error = false; + for target in targets { + let target = target.into_string().unwrap(); + match make.build_target(&target) { + Ok(updated) => { + if !updated { + println!("make: `{target}` is up to date."); + } + } + Err(err) => { + eprintln!("make: {}", err); + status_code = err.into(); + } + } + + if keep_going { + eprintln!( + "{}: Target {} not remade because of errors", + gettext("make"), + target + ); + had_error = true; + } + + if status_code != 0 { + break; + } + } + + if had_error { + status_code = 2; + } + process::exit(status_code); +} + +fn print_rules(rules: &BTreeMap>) { + print!("{:?}", rules); +} + +/// Parse the makefile at the given path, or the first default makefile found. +/// If no makefile is found, print an error message and exit. +fn parse_makefile(path: Option>) -> Result { + let path = path.as_ref().map(|p| p.as_ref()); + + let path = match path { + Some(path) => path, + None => { + let mut makefile = None; + for m in MAKEFILE_PATH.iter() { + let path = Path::new(m); + if path.exists() { + makefile = Some(path); + break; + } + } + if let Some(makefile) = makefile { + makefile + } else { + return Err(NoMakefile); + } + } + }; + + let contents = if path == Path::new("-") { + read_stdin()? + } else { + match fs::read_to_string(path) { + Ok(contents) => contents, + Err(err) => { + return Err(IoError(err.kind())); + } + } + }; + + match Makefile::from_str(&contents) { + Ok(makefile) => Ok(makefile), + Err(err) => Err(ErrorCode::ParserError { constraint: err }), + } +} + +/// Reads the makefile from `stdin` until EOF (Ctrl + D) +fn read_stdin() -> Result { + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + Ok(buffer) +} diff --git a/make/src/parser/lex.rs b/make/src/parser/lex.rs new file mode 100644 index 00000000..8dd1496d --- /dev/null +++ b/make/src/parser/lex.rs @@ -0,0 +1,206 @@ +use super::SyntaxKind; +use std::collections::HashMap; +use std::iter::Peekable; +use std::str::Chars; +use std::sync::LazyLock; + +use crate::parser::SyntaxKind::{EXPORT, INCLUDE}; +static KEYWORDS: LazyLock> = + LazyLock::new(|| HashMap::from_iter([("include", INCLUDE), ("export", EXPORT)])); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum LineType { + Recipe, + Other, +} + +pub struct Lexer<'a> { + input: Peekable>, + line_type: Option, +} + +impl<'a> Lexer<'a> { + pub fn new(input: &'a str) -> Self { + Lexer { + input: input.chars().peekable(), + line_type: None, + } + } + + fn is_whitespace(c: char) -> bool { + c == ' ' || c == '\t' + } + + fn is_newline(c: char) -> bool { + c == '\n' || c == '\r' + } + + fn is_valid_identifier_char(c: char) -> bool { + c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-' + } + + fn read_while(&mut self, predicate: F) -> String + where + F: Fn(char) -> bool, + { + let mut result = String::new(); + while let Some(&c) = self.input.peek() { + if predicate(c) { + result.push(c); + self.input.next(); + } else { + break; + } + } + result + } + + /// Retrieves the next token from the input stream, identifying its type and value + /// + /// # Returns + /// + /// An `Option<(SyntaxKind, String)>`: + /// - `Some((SyntaxKind, String))` for the next token if available. + /// - `None` if the input is exhausted. + /// + fn next_token(&mut self) -> Option<(SyntaxKind, String)> { + if let Some(&c) = self.input.peek() { + match (c, self.line_type) { + ('\t', None) => { + self.input.next(); + self.line_type = Some(LineType::Recipe); + return Some((SyntaxKind::INDENT, "\t".to_string())); + } + (_, None) => { + self.line_type = Some(LineType::Other); + } + (_, _) => {} + } + + match c { + c if Self::is_newline(c) => { + self.line_type = None; + return Some((SyntaxKind::NEWLINE, self.input.next()?.to_string())); + } + '#' => { + return Some(( + SyntaxKind::COMMENT, + self.read_while(|c| !Self::is_newline(c)), + )); + } + _ => {} + } + + match self.line_type.unwrap() { + LineType::Recipe => { + Some((SyntaxKind::TEXT, self.read_while(|c| !Self::is_newline(c)))) + } + LineType::Other => match c { + c if Self::is_whitespace(c) => { + Some((SyntaxKind::WHITESPACE, self.read_while(Self::is_whitespace))) + } + c if Self::is_valid_identifier_char(c) => { + let ident = self.read_while(Self::is_valid_identifier_char); + + if let Some(token) = KEYWORDS.get(AsRef::::as_ref(&ident)) { + Some((*token, ident)) + } else { + Some((SyntaxKind::IDENTIFIER, ident)) + } + } + '+' => { + self.input.next(); + Some((SyntaxKind::PLUS, "+".to_string())) + } + '?' => { + self.input.next(); + Some((SyntaxKind::QUESTION, "?".to_string())) + } + ':' => { + self.input.next(); + Some((SyntaxKind::COLON, ":".to_string())) + } + '=' => { + self.input.next(); + Some((SyntaxKind::EQUALS, "=".to_string())) + } + '(' => { + self.input.next(); + Some((SyntaxKind::LPAREN, "(".to_string())) + } + ')' => { + self.input.next(); + Some((SyntaxKind::RPAREN, ")".to_string())) + } + '{' => { + self.input.next(); + Some((SyntaxKind::LBRACE, "{".to_string())) + } + '}' => { + self.input.next(); + Some((SyntaxKind::RBRACE, "}".to_string())) + } + '$' => { + self.input.next(); + Some((SyntaxKind::DOLLAR, "$".to_string())) + } + ',' => { + self.input.next(); + Some((SyntaxKind::COMMA, ",".to_string())) + } + '\\' => { + self.input.next(); + Some((SyntaxKind::BACKSLASH, "\\".to_string())) + } + '"' => { + self.input.next(); + Some((SyntaxKind::DOUBLE_QUOTE, "\"".to_string())) + } + '\'' => { + self.input.next(); + Some((SyntaxKind::SINGLE_QUOTE, "'".to_string())) + } + '^' => { + self.input.next(); + Some((SyntaxKind::CARET, "^".to_string())) + } + '%' => { + self.input.next(); + Some((SyntaxKind::PERCENT, "%".to_string())) + } + '@' => { + self.input.next(); + Some((SyntaxKind::AT_SIGN, "@".to_string())) + } + '*' => { + self.input.next(); + Some((SyntaxKind::STAR, "*".to_string())) + } + '<' => { + self.input.next(); + Some((SyntaxKind::LESS, "<".to_string())) + } + c => { + self.input.next(); + Some((SyntaxKind::ERROR, c.to_string())) + } + }, + } + } else { + None + } + } +} + +impl Iterator for Lexer<'_> { + type Item = (SyntaxKind, String); + + fn next(&mut self) -> Option { + self.next_token() + } +} + +pub fn lex(input: &str) -> Vec<(SyntaxKind, String)> { + let mut lexer = Lexer::new(input); + lexer.by_ref().collect::>() +} diff --git a/make/src/parser/mod.rs b/make/src/parser/mod.rs new file mode 100644 index 00000000..c2437724 --- /dev/null +++ b/make/src/parser/mod.rs @@ -0,0 +1,82 @@ +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +pub mod lex; +pub mod parse; +pub mod preprocessor; + +pub use parse::{Identifier, Makefile, Rule, VariableDefinition}; + +/// Let's start with defining all kinds of tokens and +/// composite nodes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[allow(non_camel_case_types)] +#[repr(u16)] +pub enum SyntaxKind { + // Simple single-char AST nodes + SINGLE_QUOTE = 0, + DOUBLE_QUOTE, + WHITESPACE, + BACKSLASH, + QUESTION, + AT_SIGN, + NEWLINE, + PERCENT, + EQUALS, + DOLLAR, + LPAREN, + RPAREN, + LBRACE, + RBRACE, + COLON, + CARET, + COMMA, + LESS, + PLUS, + STAR, + TAB, + + // Keywords + INCLUDE, + EXPORT, + // This may be used as an extension to syntax + // OVERRIDE, + // UNEXPORT, + // IFDEF, + // IFNDEF, + // IFEQ, + // IFNEQ, + // ELSE, + // ENDIF, + // DEFINE, + // UNDEFINE, + // ENDEF, + IDENTIFIER, + // Unused as we have more granular tokens for different operators + // OPERATOR, + COMMENT, + INDENT, + ERROR, + TEXT, + + // composite nodes + ROOT, // The entire file + RULE, // A single rule + PREREQUISITES, + RECIPE, + VARIABLE, + EXPR, + MACRO, +} + +/// Convert our `SyntaxKind` into the rowan `SyntaxKind`. +impl From for rowan::SyntaxKind { + fn from(kind: SyntaxKind) -> Self { + Self(kind as u16) + } +} diff --git a/make/src/parser/parse.rs b/make/src/parser/parse.rs new file mode 100644 index 00000000..e9335216 --- /dev/null +++ b/make/src/parser/parse.rs @@ -0,0 +1,536 @@ +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use crate::parser::lex::lex; +use rowan::ast::AstNode; +use std::str::FromStr; + +use super::SyntaxKind::*; + +#[derive(Debug)] +pub enum Error { + Io(std::io::Error), + Parse(ParseError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match &self { + Error::Io(e) => write!(f, "IO error: {}", e), + Error::Parse(e) => write!(f, "Parse error: {}", e), + } + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +impl std::error::Error for Error {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ParseError(pub Vec); + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + for err in &self.0 { + writeln!(f, "{}", err)?; + } + Ok(()) + } +} + +impl std::error::Error for ParseError {} + +impl From for Error { + fn from(e: ParseError) -> Self { + Error::Parse(e) + } +} + +/// Second, implementing the `Language` trait teaches rowan to convert between +/// these two SyntaxKind types, allowing for a nicer SyntaxNode API where +/// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Lang {} +impl rowan::Language for Lang { + type Kind = SyntaxKind; + fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind { + unsafe { std::mem::transmute::(raw.0) } + } + fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind { + kind.into() + } +} + +/// GreenNode is an immutable tree, which is cheap to change, +/// but doesn't contain offsets and parent pointers. +use rowan::GreenNode; + +use super::SyntaxKind; +use crate::parser::preprocessor::preprocess; +/// You can construct GreenNodes by hand, but a builder +/// is helpful for top-down parsers: it maintains a stack +/// of currently in-progress nodes +use rowan::GreenNodeBuilder; + +/// The parse results are stored as a "green tree". +/// We'll discuss working with the results later +#[derive(Debug)] +pub struct Parse { + green_node: GreenNode, + #[allow(unused)] + pub errors: Vec, +} + +#[derive(Clone)] +pub struct Parsed(GreenNode); + +impl Parsed { + pub fn syntax(&self) -> SyntaxNode { + SyntaxNode::new_root(self.0.clone()) + } + + pub fn root(&self) -> Makefile { + Makefile::cast(self.syntax()).unwrap() + } +} + +pub fn parse(text: &str) -> Result { + struct Parser { + /// input tokens, including whitespace, + /// in *reverse* order. + tokens: Vec<(SyntaxKind, String)>, + /// the in-progress tree. + builder: GreenNodeBuilder<'static>, + /// the list of syntax errors we've accumulated + /// so far. + errors: Vec, + } + + impl Parser { + fn error(&mut self, msg: String) { + self.builder.start_node(ERROR.into()); + if self.current().is_some() { + self.bump(); + } + self.errors.push(msg); + self.builder.finish_node(); + } + + fn parse_expr(&mut self) { + self.builder.start_node(EXPR.into()); + loop { + match self.current() { + Some(NEWLINE) | None => { + break; + } + Some(_t) => { + self.bump(); + } + } + } + self.builder.finish_node(); + } + + fn parse_recipe_line(&mut self) { + self.builder.start_node(RECIPE.into()); + self.expect(INDENT); + self.expect(TEXT); + self.expect(NEWLINE); + self.builder.finish_node(); + } + + fn parse_include(&mut self) { + self.builder.start_node(RULE.into()); + self.expect(IDENTIFIER); + self.skip_ws(); + if self.tokens.last() == Some(&(IDENTIFIER, "include".to_string())) { + self.expect(IDENTIFIER); + self.skip_ws(); + } + self.expect(NEWLINE); + self.builder.token(IDENTIFIER.into(), "variables.mk"); + dbg!(&self.builder); + self.builder.finish_node(); + } + + fn parse_rule(&mut self) { + self.builder.start_node(RULE.into()); + self.skip_ws(); + self.try_expect(EXPORT); + self.skip_ws(); + self.expect(IDENTIFIER); + self.skip_ws(); + if self.tokens.pop() == Some((COLON, ":".to_string())) { + self.builder.token(COLON.into(), ":"); + } else { + self.error("expected ':'".into()); + } + self.skip_ws(); + self.parse_expr(); + self.expect(NEWLINE); + loop { + match self.current() { + Some(INDENT) => { + self.parse_recipe_line(); + } + Some(NEWLINE) => { + self.bump(); + break; + } + _ => { + break; + } + } + } + self.builder.finish_node(); + } + + fn parse(mut self) -> Parse { + self.builder.start_node(ROOT.into()); + loop { + match self.find(|&&(k, _)| k == COLON || k == NEWLINE || k == INCLUDE) { + Some((COLON, ":")) => { + self.parse_rule(); + } + Some((INCLUDE, "include")) => { + dbg!(&self.tokens); + self.parse_include(); + } + Some((NEWLINE, _)) => { + self.bump(); + } + Some(_) | None => { + self.error(" *** No targets. Stop.".to_string()); + if self.current().is_some() { + self.bump(); + } + } + } + + if self.current().is_none() { + break; + } + } + // Close the root node. + self.builder.finish_node(); + + // Turn the builder into a GreenNode + Parse { + green_node: self.builder.finish(), + errors: self.errors, + } + } + /// Advance one token, adding it to the current branch of the tree builder. + fn bump(&mut self) { + let (kind, text) = self.tokens.pop().unwrap(); + self.builder.token(kind.into(), text.as_str()); + } + /// Peek at the first unprocessed token + fn current(&self) -> Option { + self.tokens.last().map(|(kind, _)| *kind) + } + + fn find( + &self, + finder: impl FnMut(&&(SyntaxKind, String)) -> bool, + ) -> Option<(SyntaxKind, &str)> { + self.tokens + .iter() + .rev() + .find(finder) + .map(|(kind, text)| (*kind, text.as_str())) + } + + fn expect(&mut self, expected: SyntaxKind) { + if self.current() != Some(expected) { + self.error(format!("expected {:?}, got {:?}", expected, self.current())); + } else { + self.bump(); + } + } + + fn try_expect(&mut self, expected: SyntaxKind) -> bool { + if self.current() != Some(expected) { + false + } else { + self.bump(); + true + } + } + + fn skip_ws(&mut self) { + while self.current() == Some(WHITESPACE) { + self.bump() + } + } + } + + let mut tokens = lex(text); + + tokens.reverse(); + let result = Parser { + tokens, + builder: GreenNodeBuilder::new(), + errors: Vec::new(), + } + .parse(); + + if !result.errors.is_empty() { + Err(ParseError(result.errors)) + } else { + Ok(Parsed(result.green_node)) + } +} + +/// To work with the parse results we need a view into the +/// green tree - the Syntax tree. +/// It is also immutable, like a GreenNode, +/// but it contains parent pointers, offsets, and +/// has identity semantics. + +type SyntaxNode = rowan::SyntaxNode; +#[allow(unused)] +type SyntaxToken = rowan::SyntaxToken; +#[allow(unused)] +type SyntaxElement = rowan::NodeOrToken; + +impl Parse { + pub fn syntax(&self) -> SyntaxNode { + SyntaxNode::new_root(self.green_node.clone()) + } + + pub fn root(&self) -> Makefile { + Makefile::cast(self.syntax()).unwrap() + } +} + +macro_rules! ast_node { + ($ast:ident, $kind:ident) => { + #[derive(PartialEq, Eq, Hash)] + #[repr(transparent)] + pub struct $ast(SyntaxNode); + + impl AstNode for $ast { + type Language = Lang; + + fn can_cast(kind: SyntaxKind) -> bool { + kind == $kind + } + + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self(syntax)) + } else { + None + } + } + + fn syntax(&self) -> &SyntaxNode { + &self.0 + } + } + + impl core::fmt::Display for $ast { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + write!(f, "{}", self.0.text()) + } + } + }; +} + +ast_node!(Makefile, ROOT); +ast_node!(Rule, RULE); +ast_node!(Identifier, IDENTIFIER); +ast_node!(VariableDefinition, VARIABLE); + +impl VariableDefinition { + pub fn name(&self) -> Option { + self.syntax().children_with_tokens().find_map(|it| { + it.as_token().and_then(|it| { + if it.kind() == IDENTIFIER && it.text() != "export" { + Some(it.text().to_string()) + } else { + None + } + }) + }) + } + + pub fn raw_value(&self) -> Option { + self.syntax() + .children() + .find(|it| it.kind() == EXPR) + .map(|it| it.text().to_string()) + } +} + +impl Makefile { + pub fn new() -> Makefile { + let mut builder = GreenNodeBuilder::new(); + + builder.start_node(ROOT.into()); + builder.finish_node(); + + let syntax = SyntaxNode::new_root(builder.finish()); + Makefile(syntax.clone_for_update()) + } + + /// Read a changelog file from a reader + pub fn read(mut r: R) -> Result { + let mut buf = String::new(); + r.read_to_string(&mut buf)?; + Ok(buf.parse()?) + } + + pub fn read_relaxed(mut r: R) -> Result { + let mut buf = String::new(); + r.read_to_string(&mut buf)?; + + let parsed = parse(&buf)?; + Ok(parsed.root()) + } + + pub fn rules(&self) -> impl Iterator { + self.syntax().children().filter_map(Rule::cast) + } + + pub fn rules_by_target<'a>(&'a self, target: &'a str) -> impl Iterator + 'a { + self.rules() + .filter(move |rule| rule.targets().any(|t| t == target)) + } + + pub fn variable_definitions(&self) -> impl Iterator { + self.syntax() + .children() + .filter_map(VariableDefinition::cast) + } + + pub fn add_rule(&mut self, target: &str) -> Rule { + let mut builder = GreenNodeBuilder::new(); + builder.start_node(RULE.into()); + builder.token(IDENTIFIER.into(), target); + builder.token(COLON.into(), ":"); + builder.token(NEWLINE.into(), "\n"); + builder.finish_node(); + + let syntax = SyntaxNode::new_root(builder.finish()).clone_for_update(); + let pos = self.0.children_with_tokens().count(); + self.0 + .splice_children(pos..pos, vec![syntax.clone().into()]); + Rule(syntax) + } +} + +impl Rule { + pub fn targets(&self) -> impl Iterator { + self.syntax() + .children_with_tokens() + .take_while(|it| it.as_token().map_or(true, |t| t.kind() != COLON)) + .filter_map(|it| it.as_token().map(|t| t.text().to_string())) + } + + pub fn prerequisites(&self) -> impl Iterator { + self.syntax() + .children() + .find(|it| it.kind() == EXPR) + .into_iter() + .flat_map(|it| { + it.children_with_tokens().filter_map(|it| { + it.as_token().and_then(|t| { + if t.kind() == IDENTIFIER { + Some(t.text().to_string()) + } else { + None + } + }) + }) + }) + } + + pub fn recipes(&self) -> impl Iterator { + self.syntax() + .children() + .filter(|it| it.kind() == RECIPE) + .flat_map(|it| { + it.children_with_tokens().filter_map(|it| { + it.as_token().and_then(|t| { + if t.kind() == TEXT { + Some(t.text().to_string()) + } else { + None + } + }) + }) + }) + } + + pub fn replace_command(&self, i: usize, line: &str) { + // Find the RECIPE with index i, then replace the line in it + let index = self + .syntax() + .children() + .filter(|it| it.kind() == RECIPE) + .nth(i) + .expect("index out of bounds") + .index(); + + let mut builder = GreenNodeBuilder::new(); + builder.start_node(RECIPE.into()); + builder.token(INDENT.into(), "\t"); + builder.token(TEXT.into(), line); + builder.token(NEWLINE.into(), "\n"); + builder.finish_node(); + + let syntax = SyntaxNode::new_root(builder.finish()).clone_for_update(); + self.0 + .splice_children(index..index + 1, vec![syntax.into()]); + } + + pub fn push_command(&self, line: &str) { + // Find the latest RECIPE entry, then append the new line after it. + let index = self + .0 + .children_with_tokens() + .filter(|it| it.kind() == RECIPE) + .last(); + + let index = index.map_or_else( + || self.0.children_with_tokens().count(), + |it| it.index() + 1, + ); + + let mut builder = GreenNodeBuilder::new(); + builder.start_node(RECIPE.into()); + builder.token(INDENT.into(), "\t"); + builder.token(TEXT.into(), line); + builder.token(NEWLINE.into(), "\n"); + builder.finish_node(); + let syntax = SyntaxNode::new_root(builder.finish()).clone_for_update(); + + self.0.splice_children(index..index, vec![syntax.into()]); + } +} + +impl Default for Makefile { + fn default() -> Self { + Self::new() + } +} + +impl FromStr for Makefile { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let processed = preprocess(s).map_err(|e| ParseError(vec![e.to_string()]))?; + parse(&processed).map(|node| node.root()) + } +} diff --git a/make/src/parser/preprocessor.rs b/make/src/parser/preprocessor.rs new file mode 100644 index 00000000..a9a6076f --- /dev/null +++ b/make/src/parser/preprocessor.rs @@ -0,0 +1,325 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Display, Formatter}; +use std::fs; +use std::iter::Peekable; +use std::path::Path; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::Acquire; + +#[derive(Debug)] +pub enum PreprocError { + EmptyIdent, + UnexpectedEOF, + UnexpectedSymbol(char), + TooManyColons, + BadAssignmentOperator(char), + CommandFailed, + UndefinedMacro(String), + BadMacroName, +} + +impl Display for PreprocError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{:?}", self)?; + Ok(()) + } +} + +impl std::error::Error for PreprocError {} + +type Result = std::result::Result; + +fn skip_blank(letters: &mut Peekable>) { + while let Some(letter) = letters.peek() { + if !letter.is_whitespace() { + break; + }; + letters.next(); + } +} + +fn suitable_ident(c: &char) -> bool { + c.is_alphanumeric() || matches!(c, '_' | '.') +} + +fn get_ident(letters: &mut Peekable>) -> Result { + let mut ident = String::new(); + + while let Some(letter) = letters.peek() { + if !suitable_ident(letter) { + break; + }; + ident.push(*letter); + letters.next(); + } + + if ident.is_empty() { + Err(PreprocError::EmptyIdent) + } else { + Ok(ident) + } +} + +fn take_till_eol(letters: &mut Peekable>) -> String { + let mut content = String::new(); + + while let Some(letter) = letters.peek() { + if matches!(letter, '\n' | '#') { + break; + }; + content.push(*letter); + letters.next(); + } + + content +} + +/// Searches for all the lines in makefile that resemble macro definition +/// and creates hashtable from macro names and bodies +fn generate_macro_table( + source: &str, +) -> std::result::Result, PreprocError> { + let macro_defs = source.lines().filter(|line| line.contains('=')); + let mut macro_table = HashMap::::new(); + + for def in macro_defs { + enum Operator { + Equals, + Colon, + Colon2, + Colon3, + Bang, + QuestionMark, + Plus, + } + + let mut text = def.chars().peekable(); + + let mut macro_name = get_ident(&mut text)?; + if macro_name == "export" { + skip_blank(&mut text); + macro_name = get_ident(&mut text)?; + } + skip_blank(&mut text); + let Some(symbol) = text.next() else { + Err(PreprocError::UnexpectedEOF)? + }; + let operator = match symbol { + '=' => Operator::Equals, + ':' => { + let mut count = 1; + while let Some(':') = text.peek() { + count += 1; + text.next(); + } + let Some('=') = text.next() else { + Err(PreprocError::BadAssignmentOperator(':'))? + }; + + match count { + 1 => Operator::Colon, + 2 => Operator::Colon2, + 3 => Operator::Colon3, + _ => Err(PreprocError::TooManyColons)?, + } + } + '!' => { + let Some('=') = text.next() else { + Err(PreprocError::BadAssignmentOperator('!'))? + }; + Operator::Bang + } + '?' => { + let Some('=') = text.next() else { + Err(PreprocError::BadAssignmentOperator('?'))? + }; + Operator::QuestionMark + } + '+' => { + let Some('=') = text.next() else { + Err(PreprocError::BadAssignmentOperator('+'))? + }; + Operator::Plus + } + c => Err(PreprocError::UnexpectedSymbol(c))?, + }; + skip_blank(&mut text); + let mut macro_body = take_till_eol(&mut text); + + match operator { + Operator::Equals => {} + Operator::Colon | Operator::Colon2 => loop { + let (result, substitutions) = substitute(¯o_body, ¯o_table)?; + if substitutions == 0 { + break; + } else { + macro_body = result + } + }, + Operator::Colon3 => { + macro_body = substitute(¯o_body, ¯o_table)?.0; + } + Operator::Bang => { + macro_body = substitute(¯o_body, ¯o_table)?.0; + let Ok(result) = std::process::Command::new("sh") + .args(["-c", ¯o_body]) + .output() + else { + Err(PreprocError::CommandFailed)? + }; + macro_body = String::from_utf8_lossy(&result.stdout).to_string(); + } + Operator::QuestionMark => { + if let Some(body) = macro_table.remove(¯o_name) { + macro_body = body + } + } + Operator::Plus => { + if let Some(body) = macro_table.remove(¯o_name) { + macro_body = format!("{} {}", body, macro_body); + } + } + } + + macro_table.insert(macro_name, macro_body); + } + + Ok(macro_table) +} + +pub static ENV_MACROS: AtomicBool = AtomicBool::new(false); + +fn substitute(source: &str, table: &HashMap) -> Result<(String, u32)> { + let env_macros = ENV_MACROS.load(Acquire); + + let mut substitutions = 0; + let mut result = String::with_capacity(source.len()); + + let mut letters = source.chars().peekable(); + while let Some(letter) = letters.next() { + if letter != '$' { + result.push(letter); + continue; + } + + let Some(letter) = letters.next() else { + Err(PreprocError::UnexpectedEOF)? + }; + + match letter { + // Internal macros - we leave them "as is" + // yet as they will be dealt with in the + // parsing stage with more context available + c @ ('$' | '@' | '%' | '?' | '<' | '*') => { + result.push('$'); + result.push(c); + continue; + } + c if suitable_ident(&c) => { + let env_macro = if env_macros { + std::env::var(c.to_string()).ok() + } else { + None + }; + let table_macro = table.get(&c.to_string()).cloned(); + let Some(macro_body) = env_macro.or(table_macro) else { + Err(PreprocError::UndefinedMacro(c.to_string()))? + }; + result.push_str(¯o_body); + substitutions += 1; + continue; + } + '(' | '{' => { + skip_blank(&mut letters); + let Ok(macro_name) = get_ident(&mut letters) else { + Err(PreprocError::BadMacroName)? + }; + skip_blank(&mut letters); + let Some(finilizer) = letters.next() else { + Err(PreprocError::UnexpectedEOF)? + }; + if !matches!(finilizer, ')' | '}') { + Err(PreprocError::UnexpectedSymbol(finilizer))? + } + + let env_macro = if env_macros { + std::env::var(¯o_name).ok() + } else { + None + }; + let table_macro = table.get(¯o_name).cloned(); + let Some(macro_body) = env_macro.or(table_macro) else { + Err(PreprocError::UndefinedMacro(macro_name.to_string()))? + }; + result.push_str(¯o_body); + substitutions += 1; + + continue; + } + c => Err(PreprocError::UnexpectedSymbol(c))?, + } + } + + Ok((result, substitutions)) +} + +/// Copy-pastes included makefiles into single one recursively. +/// Pretty much the same as C preprocessor and `#include` directive +fn process_include_lines(source: &str, table: &HashMap) -> (String, usize) { + let mut counter = 0; + let result = source + .lines() + .map(|x| { + if let Some(s) = x.strip_prefix("include") { + counter += 1; + let s = s.trim(); + let (source, _) = substitute(s, table).unwrap_or_default(); + let path = Path::new(&source); + + fs::read_to_string(path).unwrap() + } else { + x.to_string() + } + }) + .map(|mut x| { + x.push('\n'); + x + }) + .collect::(); + (result, counter) +} + +fn remove_variables(source: &str) -> String { + source + .lines() + .filter(|line| !line.contains('=')) + .map(|x| { + let mut x = x.to_string(); + x.push('\n'); + x + }) + .collect::() +} + +/// Processes `include`s and macros +pub fn preprocess(source: &str) -> Result { + let mut source = source.to_string(); + let mut includes = 1; + let mut table = generate_macro_table(&source)?; + + while includes > 0 { + (source, includes) = process_include_lines(&source, &HashMap::new()); + table = generate_macro_table(&source)?; + } + + source = remove_variables(&source); + + loop { + let (result, substitutions) = substitute(&source, &table)?; + if substitutions == 0 { + break Ok(result); + } else { + source = result + } + } +} diff --git a/make/src/rule.rs b/make/src/rule.rs new file mode 100644 index 00000000..fe25cb04 --- /dev/null +++ b/make/src/rule.rs @@ -0,0 +1,338 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +pub mod config; +pub mod prerequisite; +pub mod recipe; +pub mod target; + +use crate::{ + config::Config as GlobalConfig, + error_code::ErrorCode::{self, *}, + parser::{Rule as ParsedRule, VariableDefinition}, + signal_handler, DEFAULT_SHELL, DEFAULT_SHELL_VAR, +}; +use config::Config; +use gettextrs::gettext; +use prerequisite::Prerequisite; +use recipe::config::Config as RecipeConfig; +use recipe::Recipe; +use std::collections::VecDeque; +use std::io::ErrorKind; +use std::path::PathBuf; +use std::{ + collections::HashMap, + env, + fs::{File, FileTimes}, + process::{self, Command}, + sync::{Arc, LazyLock, Mutex}, + time::SystemTime, +}; +use target::Target; + +type LazyArcMutex = LazyLock>>; + +pub static INTERRUPT_FLAG: LazyArcMutex> = + LazyLock::new(|| Arc::new(Mutex::new(None))); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Rule { + /// The targets of the rule + targets: Vec, + /// The prerequisites of the rule + prerequisites: Vec, + /// The recipe of the rule + recipes: Vec, + + pub config: Config, +} + +impl Rule { + pub fn targets(&self) -> impl Iterator { + self.targets.iter() + } + + pub fn prerequisites(&self) -> impl Iterator { + self.prerequisites.iter() + } + + pub fn recipes(&self) -> impl Iterator { + self.recipes.iter() + } + + /// Runs the rule with the global config and macros passed in. + /// + /// Returns `Ok` on success and `Err` on any errors while running the rule. + pub fn run( + &self, + global_config: &GlobalConfig, + macros: &[VariableDefinition], + target: &Target, + up_to_date: bool, + ) -> Result<(), ErrorCode> { + let GlobalConfig { + ignore: global_ignore, + dry_run: global_dry_run, + silent: global_silent, + touch: global_touch, + env_macros: global_env_macros, + quit: global_quit, + clear: _, + print: global_print, + keep_going: global_keep_going, + terminate: global_terminate, + precious: global_precious, + rules: _, + } = *global_config; + let Config { + ignore: rule_ignore, + silent: rule_silent, + precious: rule_precious, + phony: _, + } = self.config; + + let files = match target { + Target::Inference { from, to, .. } => find_files_with_extension(from)? + .into_iter() + .map(|input| { + let mut output = input.clone(); + output.set_extension(to); + (input, output) + }) + .collect::>(), + _ => { + vec![(PathBuf::from(""), PathBuf::from(""))] + } + }; + + for inout in files { + for recipe in self.recipes() { + let RecipeConfig { + ignore: recipe_ignore, + silent: recipe_silent, + force_run: recipe_force_run, + } = recipe.config; + + let ignore = global_ignore || rule_ignore || recipe_ignore; + let dry_run = global_dry_run; + let silent = global_silent || rule_silent || recipe_silent; + let force_run = recipe_force_run; + let touch = global_touch; + let env_macros = global_env_macros; + let quit = global_quit; + let print = global_print; + let precious = global_precious || rule_precious; + let keep_going = global_keep_going; + let terminate = global_terminate; + + *INTERRUPT_FLAG.lock().unwrap() = Some((target.as_ref().to_string(), precious)); + + if !ignore || print || quit || dry_run { + signal_handler::register_signals(); + } + + if !force_run { + // -n flag + if dry_run { + println!("{}", recipe); + continue; + } + + // -t flag + if touch { + continue; + } + // -q flag + if quit { + if up_to_date { + process::exit(0); + } else { + process::exit(1); + } + } + } + + // -s flag + if !silent { + println!("{}", recipe); + } + + let mut command = Command::new( + env::var(DEFAULT_SHELL_VAR) + .as_ref() + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_SHELL), + ); + + self.init_env(env_macros, &mut command, macros); + let recipe = + self.substitute_internal_macros(target, recipe, &inout, self.prerequisites()); + command.args(["-c", recipe.as_ref()]); + + let status = match command.status() { + Ok(status) => status, + Err(err) => { + if ignore { + continue; + } else { + return Err(IoError(err.kind())); + } + } + }; + if !status.success() && !ignore { + // -S and -k flags + if !terminate && keep_going { + eprintln!( + "make: {}", + ExecutionError { + exit_code: status.code(), + } + ); + break; + } else { + return Err(ExecutionError { + exit_code: status.code(), + }); + } + } + } + + let silent = global_silent || rule_silent; + let touch = global_touch; + + // -t flag + if touch { + if !silent { + println!("{} {target}", gettext("touch")); + } + let file = File::create(target.as_ref())?; + file.set_times(FileTimes::new().set_modified(SystemTime::now()))?; + return Ok(()); + } + } + + Ok(()) + } + + fn substitute_internal_macros<'a>( + &self, + target: &Target, + recipe: &Recipe, + files: &(PathBuf, PathBuf), + mut prereqs: impl Iterator, + ) -> Recipe { + let recipe = recipe.inner(); + let mut stream = recipe.chars(); + let mut result = String::new(); + + while let Some(ch) = stream.next() { + if ch != '$' { + result.push(ch); + continue; + } + + match stream.next() { + Some('@') => { + if let Some(s) = target.as_ref().split('(').next() { + result.push_str(s) + } + } + Some('%') => { + if let Some(body) = target.as_ref().split('(').nth(1) { + result.push_str(body.strip_suffix(')').unwrap_or(body)) + } + } + Some('?') => { + (&mut prereqs) + .map(|x| x.as_ref()) + .for_each(|x| result.push_str(x)); + } + Some('$') => result.push('$'), + Some('<') => result.push_str(files.0.to_str().unwrap()), + Some('*') => result.push_str(files.1.to_str().unwrap()), + Some(_) => break, + None => { + eprintln!("Unexpected `$` at the end of the rule!") + } + } + } + + Recipe::new(result) + } + + /// A helper function to initialize env vars for shell commands. + fn init_env(&self, env_macros: bool, command: &mut Command, variables: &[VariableDefinition]) { + let mut macros: HashMap = variables + .iter() + .map(|v| { + ( + v.name().unwrap_or_default(), + v.raw_value().unwrap_or_default(), + ) + }) + .collect(); + + if env_macros { + let env_vars: HashMap = std::env::vars().collect(); + macros.extend(env_vars); + } + command.envs(macros); + } +} + +impl From for Rule { + fn from(parsed: ParsedRule) -> Self { + let config = Config::default(); + Self::from((parsed, config)) + } +} + +impl From<(ParsedRule, Config)> for Rule { + fn from((parsed, config): (ParsedRule, Config)) -> Self { + let targets = parsed.targets().map(Target::new).collect(); + let prerequisites = parsed.prerequisites().map(Prerequisite::new).collect(); + let recipes = parsed.recipes().map(Recipe::new).collect(); + Rule { + targets, + prerequisites, + recipes, + config, + } + } +} + +fn find_files_with_extension(ext: &str) -> Result, ErrorCode> { + use std::{env, fs}; + + let mut result = vec![]; + let Ok(current) = env::current_dir() else { + Err(IoError(ErrorKind::PermissionDenied))? + }; + let mut dirs_to_walk = VecDeque::new(); + dirs_to_walk.push_back(current); + + while let Some(path) = dirs_to_walk.pop_front() { + let files = fs::read_dir(path)?; + for file in files.filter_map(Result::ok) { + let Ok(metadata) = file.metadata() else { + continue; + }; + + if metadata.is_file() { + if let Some(e) = file.path().extension() { + if ext == e { + result.push(file.path()); + } + } + } + } + } + + Ok(result) +} diff --git a/make/src/rule/config.rs b/make/src/rule/config.rs new file mode 100644 index 00000000..29e9e7ce --- /dev/null +++ b/make/src/rule/config.rs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// The configuration for a rule. +pub struct Config { + /// Whether to ignore the errors in the rule + pub ignore: bool, + /// Whether to print recipe lines + pub silent: bool, + /// Whether rule includes phony targets + pub phony: bool, + /// Whether rule includes precious targets + pub precious: bool, +} + +#[allow(clippy::derivable_impls)] +impl Default for Config { + fn default() -> Self { + Self { + ignore: false, + silent: false, + phony: false, + precious: false, + } + } +} diff --git a/make/src/rule/prerequisite.rs b/make/src/rule/prerequisite.rs new file mode 100644 index 00000000..04462985 --- /dev/null +++ b/make/src/rule/prerequisite.rs @@ -0,0 +1,35 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use core::fmt; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// A prerequisite for a rule. +pub struct Prerequisite { + name: String, +} + +impl Prerequisite { + /// Creates a new prerequisite with the given name. + pub fn new(name: impl Into) -> Self { + Prerequisite { name: name.into() } + } +} + +impl AsRef for Prerequisite { + fn as_ref(&self) -> &str { + &self.name + } +} + +impl fmt::Display for Prerequisite { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} diff --git a/make/src/rule/recipe.rs b/make/src/rule/recipe.rs new file mode 100644 index 00000000..551c5416 --- /dev/null +++ b/make/src/rule/recipe.rs @@ -0,0 +1,77 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +pub mod config; + +use core::fmt; +use std::collections::HashSet; + +use config::Config; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum Prefix { + Ignore, + Silent, + ForceRun, +} + +impl Prefix { + fn get_prefix(c: char) -> Option { + match c { + '-' => Some(Prefix::Ignore), + '@' => Some(Prefix::Silent), + '+' => Some(Prefix::ForceRun), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// A recipe for a rule. +pub struct Recipe { + inner: String, + + pub config: Config, +} + +impl Recipe { + /// Creates a new recipe with the given inner recipe. + pub fn new(inner: impl Into) -> Self { + let mut inner = inner.into(); + let prefixes = inner + .chars() + .map_while(Prefix::get_prefix) + .collect::>(); + + // remove the prefix from the inner; + inner.replace_range(..prefixes.len(), ""); + + Recipe { + inner, + config: Config::from(prefixes.into_iter().collect::>()), + } + } + + /// Retrieves the inner recipe. + pub fn inner(&self) -> &str { + &self.inner + } +} + +impl AsRef for Recipe { + fn as_ref(&self) -> &str { + &self.inner + } +} + +impl fmt::Display for Recipe { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.inner) + } +} diff --git a/make/src/rule/recipe/config.rs b/make/src/rule/recipe/config.rs new file mode 100644 index 00000000..59fd4888 --- /dev/null +++ b/make/src/rule/recipe/config.rs @@ -0,0 +1,47 @@ +use std::collections::HashSet; + +use super::Prefix; + +/// A recipe configuration. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Config { + /// Whether the errors should be ignored. + pub ignore: bool, + /// Whether the recipe should be silent. + pub silent: bool, + /// Whether the recipe should be forced to run even with -n, -t options + pub force_run: bool, +} + +#[allow(clippy::derivable_impls)] +impl Default for Config { + fn default() -> Self { + Config { + ignore: false, + silent: false, + force_run: false, + } + } +} + +impl From> for Config { + fn from(prefixes: HashSet) -> Self { + let mut ignore = false; + let mut silent = false; + let mut force_run = false; + + for prefix in prefixes { + match prefix { + Prefix::Ignore => ignore = true, + Prefix::Silent => silent = true, + Prefix::ForceRun => force_run = true, + } + } + + Self { + ignore, + silent, + force_run, + } + } +} diff --git a/make/src/rule/target.rs b/make/src/rule/target.rs new file mode 100644 index 00000000..17a5c570 --- /dev/null +++ b/make/src/rule/target.rs @@ -0,0 +1,113 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use crate::special_target::SpecialTarget; +use core::fmt; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// A target for a rule. +pub enum Target { + Simple { + name: &'static str, + }, + Inference { + name: &'static str, + from: &'static str, + to: &'static str, + }, + Special(SpecialTarget), +} + +impl Target { + /// Creates a new target with the given name. + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + if let Some(t) = Self::try_parse_special(&name) { + return t; + } + + if let Some(t) = Self::try_parse_inference(&name) { + return t; + } + + Target::Simple { name: name.leak() } + } + + pub fn name(&self) -> &'static str { + match self { + Target::Simple { name } => name, + Target::Inference { name, .. } => name, + Target::Special(target) => match target { + SpecialTarget::Default => ".DEFAULT", + SpecialTarget::Ignore => ".IGNORE", + SpecialTarget::Posix => ".POSIX", + SpecialTarget::Precious => ".PRECIOUS", + SpecialTarget::SccsGet => ".SCCS_GET", + SpecialTarget::Silent => ".SILENT", + SpecialTarget::Suffixes => ".SUFFIXES", + SpecialTarget::Phony => ".PHONY", + }, + } + } + + fn try_parse_special(name: &str) -> Option { + for variant in SpecialTarget::VARIANTS { + if variant.as_ref() == name { + return Some(Target::Special(variant)); + } + } + None + } + + fn try_parse_inference(s: &str) -> Option { + let mut from = String::new(); + let mut to = String::new(); + + let mut source = s.chars().peekable(); + let Some('.') = source.next() else { None? }; + + while let Some(c) = source.peek() { + match c { + c @ ('0'..='9' | 'a'..='z' | 'A'..='Z' | '_') => from.push(*c), + '.' => break, + _ => None?, + } + source.next(); + } + + let Some('.') = source.next() else { None? }; + while let Some(c) = source.peek() { + match c { + c @ ('0'..='9' | 'a'..='z' | 'A'..='Z' | '_') => to.push(*c), + '.' | ' ' | '\t' | ':' => break, + _ => None?, + } + source.next(); + } + + Some(Self::Inference { + name: format!(".{from}.{to}").leak(), + from: from.leak(), + to: to.leak(), + }) + } +} + +impl AsRef for Target { + fn as_ref(&self) -> &'static str { + self.name() + } +} + +impl fmt::Display for Target { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.clone().name()) + } +} diff --git a/make/src/signal_handler.rs b/make/src/signal_handler.rs new file mode 100644 index 00000000..e8775c60 --- /dev/null +++ b/make/src/signal_handler.rs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use std::{fs::remove_file, process}; + +use crate::rule::INTERRUPT_FLAG; +use gettextrs::gettext; +use libc::{signal, SIGHUP, SIGINT, SIGQUIT, SIGTERM}; + +/// Handles incoming signals by setting the interrupt flag and exiting the process. +pub fn handle_signals(signal_code: libc::c_int) { + let interrupt_flag = INTERRUPT_FLAG.lock().unwrap(); + if let Some((target, precious)) = interrupt_flag.as_ref() { + eprintln!("{}", gettext("make: Interrupt")); + // .PRECIOUS special target + if !precious { + eprintln!( + "{}: {} '{}'", + gettext("make"), + gettext("Deleting file"), + target + ); + if let Err(err) = remove_file(target) { + eprintln!("{}: {}", gettext("Error deleting file"), err); + } + } + } + + process::exit(128 + signal_code); +} + +pub fn register_signals() { + unsafe { + signal(SIGINT, handle_signals as usize); + signal(SIGQUIT, handle_signals as usize); + signal(SIGTERM, handle_signals as usize); + signal(SIGHUP, handle_signals as usize); + } +} diff --git a/make/src/special_target.rs b/make/src/special_target.rs new file mode 100644 index 00000000..ed4a4bfb --- /dev/null +++ b/make/src/special_target.rs @@ -0,0 +1,332 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use core::fmt; +use std::collections::BTreeSet; + +use crate::{ + error_code::ErrorCode, + rule::{target::Target, Rule}, + Make, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SpecialTarget { + Default, + Ignore, + Posix, + Phony, + Precious, + SccsGet, + Silent, + Suffixes, +} +use crate::config::Config; +use gettextrs::gettext; +use SpecialTarget::*; + +impl SpecialTarget { + // could be automated with `strum` + pub const COUNT: usize = 8; + pub const VARIANTS: [Self; Self::COUNT] = [ + Default, Ignore, Posix, Precious, SccsGet, Silent, Suffixes, Phony, + ]; +} + +impl AsRef for SpecialTarget { + fn as_ref(&self) -> &'static str { + match self { + Default => ".DEFAULT", + Ignore => ".IGNORE", + Posix => ".POSIX", + Precious => ".PRECIOUS", + SccsGet => ".SCCS_GET", + Silent => ".SILENT", + Suffixes => ".SUFFIXES", + Phony => ".PHONY", + } + } +} + +impl fmt::Display for SpecialTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_ref()) + } +} + +#[derive(Debug)] +pub struct InferenceTarget { + from: String, + to: Option, +} + +impl InferenceTarget { + pub fn from(&self) -> &str { + self.from.as_ref() + } + + pub fn to(&self) -> Option<&str> { + self.to.as_deref() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Error { + MustNotHavePrerequisites, + MustNotHaveRecipes, + + NotSupported(SpecialTarget), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Error::*; + + match self { + MustNotHavePrerequisites => { + write!( + f, + "{}", + gettext("the special target must not have prerequisites"), + ) + } + MustNotHaveRecipes => { + write!(f, "{}", gettext("the special target must not have recipes")) + } + NotSupported(target) => { + write!( + f, + "{}: '{}'", + gettext("the special target is not supported"), + target, + ) + } + } + } +} + +impl std::error::Error for Error {} + +#[derive(Debug)] +pub struct ParseError; +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", gettext("parse error")) + } +} + +impl TryFrom for SpecialTarget { + type Error = ParseError; + + fn try_from(target: Target) -> Result { + for variant in Self::VARIANTS { + if target.as_ref() == variant.as_ref() { + return Ok(variant); + } + } + Err(ParseError) + } +} + +impl TryFrom<(Target, Config)> for InferenceTarget { + type Error = ParseError; + + fn try_from((target, config): (Target, Config)) -> Result { + let map = &BTreeSet::new(); + let suffixes = config.rules.get(".SUFFIXES").unwrap_or(map); + let source = target.to_string(); + + let from = suffixes + .iter() + .filter_map(|x| source.strip_prefix(x)) + .next() + .ok_or(ParseError)? + .to_string(); + let to = suffixes + .iter() + .filter_map(|x| source.strip_prefix(x)) + .next() + .map(|x| x.to_string()); + + Ok(Self { from, to }) + } +} + +pub struct Processor<'make> { + rule: Rule, + make: &'make mut Make, +} + +pub fn process(rule: Rule, make: &mut Make) -> Result<(), ErrorCode> { + let Some(target) = rule.targets().next().cloned() else { + return Err(ErrorCode::NoTarget { target: None }); + }; + + let this = Processor { rule, make }; + + let Ok(target) = SpecialTarget::try_from(target) else { + // not an error, ignore + return Ok(()); + }; + + match target { + Default => this.process_default(), + Ignore => this.process_ignore(), + Silent => this.process_silent(), + Suffixes => this.process_suffixes(), + Phony => this.process_phony(), + Precious => this.process_precious(), + SccsGet => this.process_sccs_get(), + unsupported => Err(Error::NotSupported(unsupported)), + } + .map_err(|err| ErrorCode::SpecialTargetConstraintNotFulfilled { + target: target.to_string(), + constraint: err, + }) +} + +/// This impl block contains modifiers for special targets +impl Processor<'_> { + /// - Additive: multiple special targets can be specified in the same makefile and the effects are + /// cumulative. + fn additive(&mut self, f: impl FnMut(&mut Rule) + Clone) { + for prerequisite in self.rule.prerequisites() { + self.make + .rules + .iter_mut() + .filter(|r| r.targets().any(|t| t.as_ref() == prerequisite.as_ref())) + .for_each(f.clone()); + } + } + + /// - Global: the special target applies to all rules in the makefile if no prerequisites are + /// specified. + fn global(&mut self, f: impl FnMut(&mut Rule) + Clone) { + if self.rule.prerequisites().count() == 0 { + self.make.rules.iter_mut().for_each(f); + } + } +} + +/// This impl block contains constraint validations for special targets +impl Processor<'_> { + fn without_prerequisites(&self) -> Result<(), Error> { + if self.rule.prerequisites().count() > 0 { + return Err(Error::MustNotHavePrerequisites); + } + Ok(()) + } + + fn without_recipes(&self) -> Result<(), Error> { + if self.rule.recipes().count() > 0 { + return Err(Error::MustNotHaveRecipes); + } + Ok(()) + } +} + +/// This impl block contains processing logic for special targets +impl Processor<'_> { + fn process_default(self) -> Result<(), Error> { + self.without_prerequisites()?; + + self.make.default_rule.replace(self.rule); + + Ok(()) + } + + fn process_ignore(mut self) -> Result<(), Error> { + self.without_recipes()?; + + let what_to_do = |rule: &mut Rule| rule.config.ignore = true; + self.additive(what_to_do); + self.global(what_to_do); + + Ok(()) + } + + fn process_silent(mut self) -> Result<(), Error> { + self.without_recipes()?; + + let what_to_do = |rule: &mut Rule| rule.config.silent = true; + self.additive(what_to_do); + self.global(what_to_do); + + Ok(()) + } + + fn process_suffixes(self) -> Result<(), Error> { + let suffixes_key = Suffixes.as_ref(); + let suffixes_set = self + .rule + .prerequisites() + .map(|suffix| suffix.as_ref().to_string()) + .collect::>(); + + self.make + .config + .rules + .insert(suffixes_key.to_string(), suffixes_set); + + Ok(()) + } + fn process_phony(mut self) -> Result<(), Error> { + let suffixes_set = self + .rule + .prerequisites() + .map(|suffix| suffix.as_ref().to_string()) + .collect::>(); + + self.make + .config + .rules + .insert(Phony.as_ref().to_string(), suffixes_set); + + let what_to_do = |rule: &mut Rule| rule.config.phony = true; + self.additive(what_to_do); + self.global(what_to_do); + + Ok(()) + } + + fn process_precious(mut self) -> Result<(), Error> { + let precious_set = self + .rule + .prerequisites() + .map(|val| val.as_ref().to_string()) + .collect::>(); + + let what_to_do = |rule: &mut Rule| rule.config.precious = true; + + self.additive(what_to_do); + self.global(what_to_do); + + self.make + .config + .rules + .insert(Precious.as_ref().to_string(), precious_set); + Ok(()) + } + fn process_sccs_get(self) -> Result<(), Error> { + self.without_prerequisites()?; + + let sccs_set = self + .rule + .recipes() + .map(|val| val.as_ref().to_string()) + .collect::>(); + + self.make + .config + .rules + .insert(SccsGet.as_ref().to_string(), sccs_set); + + Ok(()) + } +} diff --git a/make/tests/integration.rs b/make/tests/integration.rs new file mode 100644 index 00000000..0598f95c --- /dev/null +++ b/make/tests/integration.rs @@ -0,0 +1,797 @@ +// +// Copyright (c) 2024 Jeff Garzik +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use std::env; +use std::fs::{remove_file, File}; +use std::io::Write; +use std::process::{Child, Command, Stdio}; + +use plib::{run_test, run_test_base, TestPlan}; +use posixutils_make::error_code::ErrorCode; + +pub fn run_test_not_comparing_error_message(plan: TestPlan) { + let output = run_test_base(&plan.cmd, &plan.args, plan.stdin_data.as_bytes()); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!(stdout, plan.expected_out); + + assert_eq!(output.status.code(), Some(plan.expected_exit_code)); + if plan.expected_exit_code == 0 { + assert!(output.status.success()); + } +} + +fn run_test_helper_without_error_message( + args: &[&str], + expected_output: &str, + expected_exit_code: i32, +) { + let str_args: Vec = args.iter().map(|s| String::from(*s)).collect(); + + run_test_not_comparing_error_message(TestPlan { + cmd: String::from("make"), + args: str_args, + stdin_data: String::new(), + expected_out: String::from(expected_output), + expected_err: String::new(), + expected_exit_code, + }); +} + +fn run_test_helper( + args: &[&str], + expected_output: &str, + expected_error: &str, + expected_exit_code: i32, +) { + let str_args: Vec = args.iter().map(|s| String::from(*s)).collect(); + + run_test(TestPlan { + cmd: String::from("make"), + args: str_args, + stdin_data: String::new(), + expected_out: String::from(expected_output), + expected_err: String::from(expected_error), + expected_exit_code, + }); +} + +fn run_test_with_stdin_helper( + args: &[&str], + stdin_data: &str, + expected_output: &str, + expected_error: &str, + expected_exit_code: i32, +) { + let str_args: Vec = args.iter().map(|s| String::from(*s)).collect(); + + run_test(TestPlan { + cmd: String::from("make"), + args: str_args, + stdin_data: String::from(stdin_data), + expected_out: String::from(expected_output), + expected_err: String::from(expected_error), + expected_exit_code, + }); +} + +fn run_test_helper_with_setup_and_destruct( + args: &[&str], + expected_output: &str, + expected_error: &str, + expected_exit_code: i32, + setup: impl FnOnce(), + destruct: impl FnOnce(), +) { + setup(); + run_test_helper(args, expected_output, expected_error, expected_exit_code); + destruct(); +} + +fn manual_test_helper(args: &[&str]) -> Child { + // Determine the binary path based on the build profile + let relpath = if cfg!(debug_assertions) { + format!("target/debug/{}", "make") + } else { + format!("target/release/{}", "make") + }; + + // Build the full path to the binary + let test_bin_path = env::current_dir() + .expect("failed to get current directory") + .parent() + .expect("failed to get parent directory") + .join(relpath); + + // Create and spawn the command + Command::new(test_bin_path) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn command") +} + +mod arguments { + + use super::*; + + #[test] + fn dash_cap_c() { + run_test_helper( + &["-C", "tests/makefiles/arguments/dash_cap_c"], + "cat works.txt\nChanged directory\n", + "", + 0, + ) + } + + #[test] + fn dash_cap_s() { + run_test_helper( + &["-SC", "tests/makefiles/arguments/dash_cap_s"], + "OK\n", + "make: execution error: 1\n", + 2, + ) + } + + #[test] + fn dash_f() { + run_test_helper( + &["-f", "tests/makefiles/arguments/dash_f.mk"], + "echo \"Changed makefile\"\nChanged makefile\n", + "", + 0, + ) + } + + #[test] + fn dash_p() { + run_test_helper( + &["-p"], + "{\".MACROS\": {\"AR=ar\", \"ARFLAGS=-rv\", \"CC=c17\", \"CFLAGS=-O 1\", \"GFLAGS=\", \"LDFLAGS=\", \"LEX=lex\", \"LFLAGS=\", \"SCCSFLAGS=\", \"SCCSGETFLAGS=-s\", \"XSI GET=get\", \"YACC=yacc\", \"YFLAGS=\"}, \".SCCS_GET\": {\"sccs $(SCCSFLAGS) get $(SCCSGETFLAGS) $@\"}, \".SUFFIXES\": {\".a\", \".c\", \".c~\", \".l\", \".l~\", \".o\", \".sh\", \".sh~\", \".y\", \".y~\"}, \"SUFFIX RULES\": {\".c.a: $(CC) -c $(CFLAGS) $<; $(AR) $(ARFLAGS) $@ $*.o; rm -f $*.o\", \".c.o: $(CC) $(CFLAGS) -c $<\", \".c: $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<\", \".l.c: $(LEX) $(LFLAGS) $<; mv lex.yy.c $@\", \".l.o: $(LEX) $(LFLAGS) $<; $(CC) $(CFLAGS) -c lex.yy.c; rm -f lex.yy.c; mv lex.yy.o $@\", \".l~.c: $(GET) $(GFLAGS) -p $< > $*.l; $(LEX) $(LFLAGS) $*.l; mv lex.yy.c $@\", \".l~.o: $(GET) $(GFLAGS) -p $< > $*.l; $(LEX) $(LFLAGS) $*.l; $(CC) $(CFLAGS) -c lex.yy.c; rm -f lex.yy.c; mv lex.yy.o $@\", \".sh: chmod a+x $@\", \".sh: cp $< $@\", \".y.c: $(YACC) $(YFLAGS) $<; mv y.tab.c $@\", \".y.o: $(YACC) $(YFLAGS) $<; $(CC) $(CFLAGS) -c y.tab.c; rm -f y.tab.c; mv y.tab.o $@\", \".y~.c: $(GET) $(GFLAGS) -p $< > $*.y; $(YACC) $(YFLAGS) $*.y; mv y.tab.c $@\", \".y~.o: $(GET) $(GFLAGS) -p $< > $*.y; $(YACC) $(YFLAGS) $*.y; $(CC) $(CFLAGS) -c y.tab.c; rm -f y.tab.c; mv y.tab.o $@\", \"XSI .c~.o: $(GET) $(GFLAGS) -p $< > $*.c; $(CC) $(CFLAGS) -c $*.c\"}}", + "", + 0, + ) + } + #[test] + fn dash_p_with_mk() { + run_test_helper( + &["-pf", "tests/makefiles/arguments/dash_p/with_phony.mk"], + "{\".MACROS\": {\"AR=ar\", \"ARFLAGS=-rv\", \"CC=c17\", \"CFLAGS=-O 1\", \"GFLAGS=\", \"LDFLAGS=\", \"LEX=lex\", \"LFLAGS=\", \"SCCSFLAGS=\", \"SCCSGETFLAGS=-s\", \"XSI GET=get\", \"YACC=yacc\", \"YFLAGS=\"}, \".PHONY\": {\"clean\"}, \".SCCS_GET\": {\"sccs $(SCCSFLAGS) get $(SCCSGETFLAGS) $@\"}, \".SUFFIXES\": {\".a\", \".c\", \".c~\", \".l\", \".l~\", \".o\", \".sh\", \".sh~\", \".y\", \".y~\"}, \"SUFFIX RULES\": {\".c.a: $(CC) -c $(CFLAGS) $<; $(AR) $(ARFLAGS) $@ $*.o; rm -f $*.o\", \".c.o: $(CC) $(CFLAGS) -c $<\", \".c: $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<\", \".l.c: $(LEX) $(LFLAGS) $<; mv lex.yy.c $@\", \".l.o: $(LEX) $(LFLAGS) $<; $(CC) $(CFLAGS) -c lex.yy.c; rm -f lex.yy.c; mv lex.yy.o $@\", \".l~.c: $(GET) $(GFLAGS) -p $< > $*.l; $(LEX) $(LFLAGS) $*.l; mv lex.yy.c $@\", \".l~.o: $(GET) $(GFLAGS) -p $< > $*.l; $(LEX) $(LFLAGS) $*.l; $(CC) $(CFLAGS) -c lex.yy.c; rm -f lex.yy.c; mv lex.yy.o $@\", \".sh: chmod a+x $@\", \".sh: cp $< $@\", \".y.c: $(YACC) $(YFLAGS) $<; mv y.tab.c $@\", \".y.o: $(YACC) $(YFLAGS) $<; $(CC) $(CFLAGS) -c y.tab.c; rm -f y.tab.c; mv y.tab.o $@\", \"some\n.y~.c: $(GET) $(GFLAGS) -p $< > $*.y; $(YACC) $(YFLAGS) $*.y; mv y.tab.c $@\", \".y~.o: $(GET) $(GFLAGS) -p $< > $*.y; $(YACC) $(YFLAGS) $*.y; $(CC) $(CFLAGS) -c y.tab.c; rm -f y.tab.c; mv y.tab.o $@\", \"XSI .c~.o: $(GET) $(GFLAGS) -p $< > $*.c; $(CC) $(CFLAGS) -c $*.c\"}}", + "", + 0, + ) + } + + #[test] + fn dash_r() { + run_test_helper( + &["-r"], + "", + "make: no makefile\n", + ErrorCode::NoMakefile.into(), + ) + } + + #[test] + fn dash_r_with_file() { + run_test_helper_with_setup_and_destruct( + &["-rf", "tests/makefiles/arguments/dash_r/with_file.mk"], + "Converting testfile.txt to testfile.out\n", + "", + 0, + || { + File::create("testfile.txt").expect("failed to create file"); + }, + || { + remove_file("testfile.txt").expect("failed to remove file"); + remove_file("testfile.out").expect("failed to remove file"); + }, + ); + } + + #[test] + fn dash_i() { + run_test_helper( + &["-if", "tests/makefiles/arguments/dash_i.mk"], + "exit 1\necho Ignored\nIgnored\n", + "", + 0, + ) + } + + #[test] + fn dash_n() { + run_test_helper( + &["-nf", "tests/makefiles/arguments/dash_n.mk"], + "exit 1\n", + "", + 0, + ); + } + + #[test] + fn dash_s() { + run_test_helper( + &["-sf", "tests/makefiles/arguments/dash_s.mk"], + "Silent\n", + "", + 0, + ) + } + + #[test] + fn dash_t() { + let remove_touches = || { + let dir = "tests/makefiles/arguments/dash_t"; + for i in 1..=2 { + let _ = remove_file(format!("{dir}/rule{i}")); + } + }; + + run_test_helper_with_setup_and_destruct( + &["-tC", "tests/makefiles/arguments/dash_t/"], + "touch rule2\ntouch rule1\n", + "", + 0, + remove_touches, + remove_touches, + ) + } + + #[test] + fn dash_q() { + run_test_helper( + &["-qf", "tests/makefiles/arguments/dash_q/cc_target.mk"], + "", + "", + 1, + ); + } + #[test] + fn dash_k() { + run_test_helper( + &["-kf", "tests/makefiles/arguments/dash_k.mk"], + "OK\necho 12\n12\n", + "make: execution error: 1\nmake: Target z not remade because of errors\n", + 2, + ); + } +} + +// such tests should be moved directly to the package responsible for parsing makefiles +mod parsing { + + use super::*; + + #[test] + fn empty() { + run_test_helper( + &["-f", "tests/makefiles/parsing/empty.mk"], + "", + "make: parse error: *** No targets. Stop.\n\n", + 4, + ); + } + + #[test] + fn comments() { + run_test_helper( + &["-sf", "tests/makefiles/parsing/comments.mk"], + "This program should not produce any errors.\n", + "", + 0, + ); + } + + // #[test] + // #[ignore] + // fn suffixes_with_no_target() { + // run_test_helper( + // &["-f", "tests/makefiles/parsing/suffixes_with_no_targets.mk"], + // "", + // "make: parse error: No Targets", + // ErrorCode::ParseError("no targets".into()).into(), + // ); + // } +} + +mod io { + use std::io; + + use super::*; + + #[test] + fn file_not_found() { + run_test_helper( + &["-f", "tests/makefiles/does_not_exist.mk"], + "", + "make: io error: entity not found\n", + ErrorCode::IoError(io::ErrorKind::NotFound).into(), + ); + } + + #[test] + fn stdin() { + run_test_with_stdin_helper( + &["-sf", "-"], + "rule:\n\techo executed\n", + "executed\n", + "", + 0, + ) + } +} + +mod macros { + use std::env; + + use super::*; + + #[test] + fn substitutes_in_recipes() { + run_test_helper( + &["-sf", "tests/makefiles/macros/substitutes_in_recipes.mk"], + "Macros substitution works.\n", + "", + 0, + ); + } + + #[test] + fn envs_in_recipes() { + run_test_helper_with_setup_and_destruct( + &["-esf", "tests/makefiles/macros/envs_in_recipes.mk"], + "macro is replaced succesfully\n", + "", + 0, + set_env_vars, + clean_env_vars, + ); + + fn set_env_vars() { + env::set_var("MACRO", "echo"); + } + + fn clean_env_vars() { + env::remove_var("MACRO"); + } + } +} + +mod target_behavior { + use super::*; + use libc::{kill, SIGINT}; + use posixutils_make::parser::parse::ParseError; + use std::{thread, time::Duration}; + + #[test] + fn no_targets() { + run_test_helper( + &["-f", "tests/makefiles/target_behavior/no_targets.mk"], + "", + "make: parse error: *** No targets. Stop.\n\n", + ErrorCode::ParserError { + constraint: ParseError(vec![]), + } + .into(), + ); + } + + #[test] + fn makefile_priority() { + run_test_helper( + &[ + "-sC", + "tests/makefiles/target_behavior/makefile_priority/little_makefile", + ], + "makefile\n", + "", + 0, + ); + + run_test_helper( + &[ + "-sC", + "tests/makefiles/target_behavior/makefile_priority/big_Makefile", + ], + "Makefile\n", + "", + 0, + ); + } + + #[test] + fn basic_chaining() { + run_test_helper( + &["-sf", "tests/makefiles/target_behavior/basic_chaining.mk"], + "rule2\nrule1\n", + "", + 0, + ); + } + + #[test] + fn diamond_chaining_with_touches() { + let remove_touches = || { + let dir = "tests/makefiles/target_behavior/diamond_chaining_with_touches"; + for i in 1..=5 { + let _ = remove_file(format!("{}/rule{}", dir, i)); + } + }; + + run_test_helper_with_setup_and_destruct( + &[ + "-sC", + "tests/makefiles/target_behavior/diamond_chaining_with_touches", + ], + "rule4\nrule2\nrule3\nrule1\n", + "", + 0, + remove_touches, + remove_touches, + ); + } + + #[test] + fn recursive_chaining() { + run_test_helper( + &[ + "-sf", + "tests/makefiles/target_behavior/recursive_chaining.mk", + ], + "", + "make: recursive prerequisite found trying to build 'rule1'\n", + ErrorCode::RecursivePrerequisite { + origin: "rule1".into(), + } + .into(), + ); + } + + #[test] + fn async_events() { + let args = [ + "-f", + "tests/makefiles/target_behavior/async_events/signal.mk", + ]; + let child = manual_test_helper(&args); + let pid = child.id() as i32; + + thread::spawn(move || { + thread::sleep(Duration::from_millis(100)); + unsafe { + kill(pid, SIGINT); + } + }); + + let output = child.wait_with_output().expect("failed to wait for child"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!(stdout, "echo \"hello\"\nhello\ntouch text.txt\nsleep 1\n"); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert_eq!(stderr, "make: Interrupt\nmake: Deleting file 'text.txt'\n"); + + assert_eq!(output.status.code(), Some(130)); + } +} + +mod recipes { + use super::*; + + mod prefixes { + use super::*; + + #[test] + fn ignore() { + run_test_helper( + &["-f", "tests/makefiles/recipes/prefixes/ignore.mk"], + "exit 1\necho ignored\nignored\n", + "", + 0, + ); + } + + #[test] + fn silent() { + run_test_helper( + &["-f", "tests/makefiles/recipes/prefixes/silent.mk"], + "silent\n", + "", + 0, + ); + } + + mod force_run { + use super::*; + + #[test] + fn with_dry_run() { + run_test_helper( + &[ + "-snf", + "tests/makefiles/recipes/prefixes/force_run/with_dry_run.mk", + ], + "I am NOT skipped\n", + "", + 0, + ); + } + + #[test] + fn with_touch() { + let remove_touches = || { + let _ = + remove_file("tests/makefiles/recipes/prefixes/force_run/with_touch/rule"); + }; + + run_test_helper_with_setup_and_destruct( + &[ + "-stC", + "tests/makefiles/recipes/prefixes/force_run/with_touch", + ], + "I am NOT skipped\n", + "", + 0, + remove_touches, + remove_touches, + ); + } + #[test] + fn with_dash_q() { + run_test_helper( + &[ + "-sqf", + "tests/makefiles/recipes/prefixes/force_run/with_dry_run.mk", + ], + "I am NOT skipped\n", + "", + 0, + ); + } + } + + #[test] + fn multiple() { + run_test_helper( + &["-f", "tests/makefiles/recipes/prefixes/multiple.mk"], + "ignored\n", + "", + 0, + ); + } + } +} + +mod special_targets { + use super::*; + use libc::{kill, SIGINT}; + use posixutils_make::special_target; + use std::fs::remove_dir; + use std::{fs, thread, time::Duration}; + + #[test] + fn default() { + run_test_helper( + &[ + "-f", + "tests/makefiles/special_targets/default.mk", + "nonexisting_target", + ], + "echo Default\nDefault\n", + "", + 0, + ); + } + + #[test] + fn ignore() { + run_test_helper( + &["-f", "tests/makefiles/special_targets/ignore.mk"], + "exit 1\necho \"Ignored\"\nIgnored\n", + "", + 0, + ); + } + + #[test] + fn silent() { + run_test_helper( + &["-f", "tests/makefiles/special_targets/silent.mk"], + "I'm silent\n", + "", + 0, + ); + } + + #[test] + fn phony() { + run_test_helper_without_error_message( + &["-f", "tests/makefiles/special_targets/phony/phony_basic.mk"], + "rm temp\n", + 2, + ); + } + + #[test] + fn sccs_get() { + run_test_helper( + &["-pf", "tests/makefiles/special_targets/sccs/basic_sccs.mk"], + "{\".MACROS\": {\"AR=ar\", \"ARFLAGS=-rv\", \"CC=c17\", \"CFLAGS=-O 1\", \"GFLAGS=\", \"LDFLAGS=\", \"LEX=lex\", \"LFLAGS=\", \"SCCSFLAGS=\", \"SCCSGETFLAGS=-s\", \"XSI GET=get\", \"YACC=yacc\", \"YFLAGS=\"}, \".SCCS_GET\": {\"echo \\\"executing command\\\"\"}, \".SUFFIXES\": {\".a\", \".c\", \".c~\", \".l\", \".l~\", \".o\", \".sh\", \".sh~\", \".y\", \".y~\"}, \"SUFFIX RULES\": {\".c.a: $(CC) -c $(CFLAGS) $<; $(AR) $(ARFLAGS) $@ $*.o; rm -f $*.o\", \".c.o: $(CC) $(CFLAGS) -c $<\", \".c: $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<\", \".l.c: $(LEX) $(LFLAGS) $<; mv lex.yy.c $@\", \".l.o: $(LEX) $(LFLAGS) $<; $(CC) $(CFLAGS) -c lex.yy.c; rm -f lex.yy.c; mv lex.yy.o $@\", \".l~.c: $(GET) $(GFLAGS) -p $< > $*.l; $(LEX) $(LFLAGS) $*.l; mv lex.yy.c $@\", \".l~.o: $(GET) $(GFLAGS) -p $< > $*.l; $(LEX) $(LFLAGS) $*.l; $(CC) $(CFLAGS) -c lex.yy.c; rm -f lex.yy.c; mv lex.yy.o $@\", \".sh: chmod a+x $@\", \".sh: cp $< $@\", \".y.c: $(YACC) $(YFLAGS) $<; mv y.tab.c $@\", \".y.o: $(YACC) $(YFLAGS) $<; $(CC) $(CFLAGS) -c y.tab.c; rm -f y.tab.c; mv y.tab.o $@\", \"something\n.y~.c: $(GET) $(GFLAGS) -p $< > $*.y; $(YACC) $(YFLAGS) $*.y; mv y.tab.c $@\", \".y~.o: $(GET) $(GFLAGS) -p $< > $*.y; $(YACC) $(YFLAGS) $*.y; $(CC) $(CFLAGS) -c y.tab.c; rm -f y.tab.c; mv y.tab.o $@\", \"XSI .c~.o: $(GET) $(GFLAGS) -p $< > $*.c; $(CC) $(CFLAGS) -c $*.c\"}}", + "", + 0, + ); + } + + #[test] + fn precious() { + let args = [ + "-f", + "tests/makefiles/special_targets/precious/basic_precious.mk", + ]; + let child = manual_test_helper(&args); + let pid = child.id() as i32; + + thread::spawn(move || { + thread::sleep(Duration::from_millis(100)); + unsafe { + kill(pid, SIGINT); + } + }); + + let output = child.wait_with_output().expect("failed to wait for child"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!( + stdout, + "echo hello\nhello\nmkdir preciousdir\ntouch preciousdir/some.txt\nsleep 1\n" + ); + assert!(fs::exists("preciousdir/some.txt").unwrap()); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert_eq!(stderr, "make: Interrupt\n"); + + assert_eq!(output.status.code(), Some(130)); + + remove_file("preciousdir/some.txt").unwrap(); + remove_dir("preciousdir").unwrap(); + } + + #[test] + fn suffixes() { + run_test_helper_with_setup_and_destruct( + &[ + "-f", + "tests/makefiles/special_targets/suffixes/suffixes_basic.mk", + ], + "Converting copied.txt to copied.out\n", + "", + 0, + create_txt, + remove_files, + ); + + fn create_txt() { + let dir = env::current_dir().unwrap(); + for file in dir.read_dir().unwrap().map(Result::unwrap) { + if !file.file_type().map(|x| x.is_file()).unwrap_or(false) { + continue; + } + if file.path().extension().map(|x| x == "txt").unwrap_or(false) { + remove_file(file.path()).unwrap(); + } + } + + File::create("copied.txt") + .unwrap() + .write_all(b"some content") + .unwrap(); + } + + fn remove_files() { + remove_file("copied.txt").unwrap(); + remove_file("copied.out").unwrap(); + } + } + + // unspecified stderr and error type, must be refactored and improved + // #[test] + // #[ignore] + // fn clear_suffixes() { + // run_test_helper( + // &[ + // "-f", + // "tests/makefiles/special_targets/suffixes/clear_suffixes.mk", + // ], + // "Converting $< to \n", + // "make: Nothing be dobe for copied.out", + // ErrorCode::ParseError("the inner value does not matter for now".into()).into(), + // ); + // } + + mod validations { + use super::*; + + #[test] + fn without_prerequisites() { + run_test_helper( + &["-f", "tests/makefiles/special_targets/validations/without_prerequisites.mk"], + "", + "make: '.DEFAULT' special target constraint is not fulfilled: the special target must not have prerequisites\n", + ErrorCode::SpecialTargetConstraintNotFulfilled { + target: String::default(), + constraint: special_target::Error::MustNotHavePrerequisites, + } + .into(), + ); + } + + #[test] + fn without_recipes() { + run_test_helper( + &["-f", "tests/makefiles/special_targets/validations/without_recipes.mk"], + "", + "make: '.SILENT' special target constraint is not fulfilled: the special target must not have recipes\n", + ErrorCode::SpecialTargetConstraintNotFulfilled { + target: String::default(), + constraint: special_target::Error::MustNotHaveRecipes, + } + .into(), + ); + } + } + + mod modifiers { + use super::*; + + #[test] + fn additive() { + run_test_helper( + &[ + "-f", + "tests/makefiles/special_targets/modifiers/additive.mk", + ], + "I'm silent\nMe too\n", + "", + 0, + ); + } + + #[test] + fn global() { + run_test_helper( + &["-f", "tests/makefiles/special_targets/modifiers/global.mk"], + "I'm silent\n", + "", + 0, + ); + } + } + + mod behavior { + use super::*; + + #[test] + fn ignores_special_targets_as_first_target() { + run_test_helper( + &[ + "-f", + "tests/makefiles/special_targets/behavior/ignores_special_targets_as_first_target.mk", + ], + "I'm silent\n", + "", + 0, + ); + } + } +} diff --git a/make/tests/makefiles/arguments/dash_cap_c/makefile b/make/tests/makefiles/arguments/dash_cap_c/makefile new file mode 100644 index 00000000..55d05a3a --- /dev/null +++ b/make/tests/makefiles/arguments/dash_cap_c/makefile @@ -0,0 +1,2 @@ +rule: + cat works.txt diff --git a/make/tests/makefiles/arguments/dash_cap_c/works.txt b/make/tests/makefiles/arguments/dash_cap_c/works.txt new file mode 100644 index 00000000..96a71719 --- /dev/null +++ b/make/tests/makefiles/arguments/dash_cap_c/works.txt @@ -0,0 +1 @@ +Changed directory diff --git a/make/tests/makefiles/arguments/dash_cap_s/makefile b/make/tests/makefiles/arguments/dash_cap_s/makefile new file mode 100644 index 00000000..15c008bb --- /dev/null +++ b/make/tests/makefiles/arguments/dash_cap_s/makefile @@ -0,0 +1,9 @@ +all: bar baz rof +bar: + @echo OK + @false + @echo Not reached +baz: + @: +rof: + echo 12 diff --git a/make/tests/makefiles/arguments/dash_f.mk b/make/tests/makefiles/arguments/dash_f.mk new file mode 100644 index 00000000..7a75b423 --- /dev/null +++ b/make/tests/makefiles/arguments/dash_f.mk @@ -0,0 +1,2 @@ +rule: + echo "Changed makefile" diff --git a/make/tests/makefiles/arguments/dash_i.mk b/make/tests/makefiles/arguments/dash_i.mk new file mode 100644 index 00000000..51096071 --- /dev/null +++ b/make/tests/makefiles/arguments/dash_i.mk @@ -0,0 +1,3 @@ +rule: + exit 1 + echo Ignored diff --git a/make/tests/makefiles/arguments/dash_k.mk b/make/tests/makefiles/arguments/dash_k.mk new file mode 100644 index 00000000..4885c6f0 --- /dev/null +++ b/make/tests/makefiles/arguments/dash_k.mk @@ -0,0 +1,9 @@ +z: bar baz rof +bar: + @echo OK + @false + @echo Not reached +baz: + @: +rof: + echo 12 diff --git a/make/tests/makefiles/arguments/dash_n.mk b/make/tests/makefiles/arguments/dash_n.mk new file mode 100644 index 00000000..a1a77490 --- /dev/null +++ b/make/tests/makefiles/arguments/dash_n.mk @@ -0,0 +1,2 @@ +rule: + exit 1 diff --git a/make/tests/makefiles/arguments/dash_p/with_phony.mk b/make/tests/makefiles/arguments/dash_p/with_phony.mk new file mode 100644 index 00000000..a9563ca3 --- /dev/null +++ b/make/tests/makefiles/arguments/dash_p/with_phony.mk @@ -0,0 +1,4 @@ +.PHONY: clean +clean: + @echo some + diff --git a/make/tests/makefiles/arguments/dash_q/blah.c b/make/tests/makefiles/arguments/dash_q/blah.c new file mode 100644 index 00000000..b28cbb39 --- /dev/null +++ b/make/tests/makefiles/arguments/dash_q/blah.c @@ -0,0 +1,2 @@ +// blah.c +int main() { return 1; } diff --git a/make/tests/makefiles/arguments/dash_q/cc_target.mk b/make/tests/makefiles/arguments/dash_q/cc_target.mk new file mode 100644 index 00000000..842bd98d --- /dev/null +++ b/make/tests/makefiles/arguments/dash_q/cc_target.mk @@ -0,0 +1,2 @@ +blah: + cc blah.c -o blah diff --git a/make/tests/makefiles/arguments/dash_r/with_file.mk b/make/tests/makefiles/arguments/dash_r/with_file.mk new file mode 100644 index 00000000..ceda4755 --- /dev/null +++ b/make/tests/makefiles/arguments/dash_r/with_file.mk @@ -0,0 +1,10 @@ +.SUFFIXES: .txt .out + +.txt.out: + @echo "Converting testfile.txt to testfile.out" + @cp testfile.txt testfile.out + + +testfile.out: testfile.txt + + diff --git a/make/tests/makefiles/arguments/dash_s.mk b/make/tests/makefiles/arguments/dash_s.mk new file mode 100644 index 00000000..2df66e76 --- /dev/null +++ b/make/tests/makefiles/arguments/dash_s.mk @@ -0,0 +1,2 @@ +rule: + echo "Silent" diff --git a/make/tests/makefiles/arguments/dash_t/makefile b/make/tests/makefiles/arguments/dash_t/makefile new file mode 100644 index 00000000..621bfe78 --- /dev/null +++ b/make/tests/makefiles/arguments/dash_t/makefile @@ -0,0 +1,5 @@ +rule1: rule2 + echo rule1 + +rule2: + echo rule2 diff --git a/make/tests/makefiles/macros/envs_in_recipes.mk b/make/tests/makefiles/macros/envs_in_recipes.mk new file mode 100644 index 00000000..839d4449 --- /dev/null +++ b/make/tests/makefiles/macros/envs_in_recipes.mk @@ -0,0 +1,5 @@ +target: + ${MACRO} "macro is replaced succesfully" + +MACRO=ls + diff --git a/make/tests/makefiles/macros/substitutes_in_recipes.mk b/make/tests/makefiles/macros/substitutes_in_recipes.mk new file mode 100644 index 00000000..8f3ed258 --- /dev/null +++ b/make/tests/makefiles/macros/substitutes_in_recipes.mk @@ -0,0 +1,4 @@ +target: + ${PRINT} "Macros substitution works." + +PRINT=echo diff --git a/make/tests/makefiles/parsing/comments.mk b/make/tests/makefiles/parsing/comments.mk new file mode 100644 index 00000000..187023b5 --- /dev/null +++ b/make/tests/makefiles/parsing/comments.mk @@ -0,0 +1,2 @@ +comment: # this is a comment + echo "This program should not produce any errors." diff --git a/make/tests/makefiles/parsing/empty.mk b/make/tests/makefiles/parsing/empty.mk new file mode 100644 index 00000000..e69de29b diff --git a/make/tests/makefiles/parsing/include/Makefile b/make/tests/makefiles/parsing/include/Makefile new file mode 100644 index 00000000..364b6253 --- /dev/null +++ b/make/tests/makefiles/parsing/include/Makefile @@ -0,0 +1,4 @@ +include variables.mk + +all: + echo 12 diff --git a/make/tests/makefiles/parsing/include/variables.mk b/make/tests/makefiles/parsing/include/variables.mk new file mode 100644 index 00000000..5114bacb --- /dev/null +++ b/make/tests/makefiles/parsing/include/variables.mk @@ -0,0 +1,2 @@ +MESSAGE = Hello, world! + diff --git a/make/tests/makefiles/parsing/suffixes_with_no_targets.mk b/make/tests/makefiles/parsing/suffixes_with_no_targets.mk new file mode 100644 index 00000000..9adc5bb5 --- /dev/null +++ b/make/tests/makefiles/parsing/suffixes_with_no_targets.mk @@ -0,0 +1,4 @@ +.txt.out: + @echo "Converting $< to $@" + @cp copied.txt copied.out + diff --git a/make/tests/makefiles/recipes/prefixes/force_run/with_dry_run.mk b/make/tests/makefiles/recipes/prefixes/force_run/with_dry_run.mk new file mode 100644 index 00000000..6e55dfa9 --- /dev/null +++ b/make/tests/makefiles/recipes/prefixes/force_run/with_dry_run.mk @@ -0,0 +1,2 @@ +rule: + +echo "I am NOT skipped" diff --git a/make/tests/makefiles/recipes/prefixes/force_run/with_touch/makefile b/make/tests/makefiles/recipes/prefixes/force_run/with_touch/makefile new file mode 100644 index 00000000..6e55dfa9 --- /dev/null +++ b/make/tests/makefiles/recipes/prefixes/force_run/with_touch/makefile @@ -0,0 +1,2 @@ +rule: + +echo "I am NOT skipped" diff --git a/make/tests/makefiles/recipes/prefixes/ignore.mk b/make/tests/makefiles/recipes/prefixes/ignore.mk new file mode 100644 index 00000000..ab8f17eb --- /dev/null +++ b/make/tests/makefiles/recipes/prefixes/ignore.mk @@ -0,0 +1,3 @@ +rule: + -exit 1 + echo ignored diff --git a/make/tests/makefiles/recipes/prefixes/multiple.mk b/make/tests/makefiles/recipes/prefixes/multiple.mk new file mode 100644 index 00000000..86cf5ad2 --- /dev/null +++ b/make/tests/makefiles/recipes/prefixes/multiple.mk @@ -0,0 +1,3 @@ +rule: + -@+@@-+exit 1 + @echo ignored diff --git a/make/tests/makefiles/recipes/prefixes/silent.mk b/make/tests/makefiles/recipes/prefixes/silent.mk new file mode 100644 index 00000000..06c468da --- /dev/null +++ b/make/tests/makefiles/recipes/prefixes/silent.mk @@ -0,0 +1,2 @@ +rule: + @echo silent diff --git a/make/tests/makefiles/special_targets/behavior/ignores_special_targets_as_first_target.mk b/make/tests/makefiles/special_targets/behavior/ignores_special_targets_as_first_target.mk new file mode 100644 index 00000000..c0f6ccfa --- /dev/null +++ b/make/tests/makefiles/special_targets/behavior/ignores_special_targets_as_first_target.mk @@ -0,0 +1,4 @@ +.SILENT: t + +t: + echo "I'm silent" diff --git a/make/tests/makefiles/special_targets/default.mk b/make/tests/makefiles/special_targets/default.mk new file mode 100644 index 00000000..e9f8361a --- /dev/null +++ b/make/tests/makefiles/special_targets/default.mk @@ -0,0 +1,5 @@ +rule: + echo "I'm a rule" + +.DEFAULT: + echo Default diff --git a/make/tests/makefiles/special_targets/ignore.mk b/make/tests/makefiles/special_targets/ignore.mk new file mode 100644 index 00000000..f15e24e5 --- /dev/null +++ b/make/tests/makefiles/special_targets/ignore.mk @@ -0,0 +1,5 @@ +t: + exit 1 + echo "Ignored" + +.IGNORE: t diff --git a/make/tests/makefiles/special_targets/modifiers/additive.mk b/make/tests/makefiles/special_targets/modifiers/additive.mk new file mode 100644 index 00000000..bcdef73a --- /dev/null +++ b/make/tests/makefiles/special_targets/modifiers/additive.mk @@ -0,0 +1,9 @@ +.SILENT: rule1 + +rule1: rule2 + echo "Me too" + +rule2: + echo "I'm silent" + +.SILENT: rule2 diff --git a/make/tests/makefiles/special_targets/modifiers/global.mk b/make/tests/makefiles/special_targets/modifiers/global.mk new file mode 100644 index 00000000..9efb520e --- /dev/null +++ b/make/tests/makefiles/special_targets/modifiers/global.mk @@ -0,0 +1,4 @@ +.SILENT: + +rule: + echo "I'm silent" diff --git a/make/tests/makefiles/special_targets/phony/phony_basic.mk b/make/tests/makefiles/special_targets/phony/phony_basic.mk new file mode 100644 index 00000000..f1c0f928 --- /dev/null +++ b/make/tests/makefiles/special_targets/phony/phony_basic.mk @@ -0,0 +1,3 @@ +.PHONY: clean +clean: + rm temp diff --git a/make/tests/makefiles/special_targets/precious/basic_precious.mk b/make/tests/makefiles/special_targets/precious/basic_precious.mk new file mode 100644 index 00000000..39a7da28 --- /dev/null +++ b/make/tests/makefiles/special_targets/precious/basic_precious.mk @@ -0,0 +1,7 @@ +.PRECIOUS: +text.txt: + echo hello + mkdir preciousdir + touch preciousdir/some.txt + sleep 1 + echo bye diff --git a/make/tests/makefiles/special_targets/precious/empty_precious.mk b/make/tests/makefiles/special_targets/precious/empty_precious.mk new file mode 100644 index 00000000..2b48a8bc --- /dev/null +++ b/make/tests/makefiles/special_targets/precious/empty_precious.mk @@ -0,0 +1 @@ +.PRECIOUS: diff --git a/make/tests/makefiles/special_targets/sccs/basic_sccs.mk b/make/tests/makefiles/special_targets/sccs/basic_sccs.mk new file mode 100644 index 00000000..f197967b --- /dev/null +++ b/make/tests/makefiles/special_targets/sccs/basic_sccs.mk @@ -0,0 +1,4 @@ +.SCCS_GET: + @echo "executing command" +target: + @echo something diff --git a/make/tests/makefiles/special_targets/silent.mk b/make/tests/makefiles/special_targets/silent.mk new file mode 100644 index 00000000..f631c904 --- /dev/null +++ b/make/tests/makefiles/special_targets/silent.mk @@ -0,0 +1,4 @@ +t: + echo "I'm silent" + +.SILENT: t diff --git a/make/tests/makefiles/special_targets/suffixes/clear_suffixes.mk b/make/tests/makefiles/special_targets/suffixes/clear_suffixes.mk new file mode 100644 index 00000000..cec659ed --- /dev/null +++ b/make/tests/makefiles/special_targets/suffixes/clear_suffixes.mk @@ -0,0 +1,9 @@ +.SUFFIXES: + +.txt.out: + @echo "Converting $< to $@" + @cp copied.txt copied.out + + +copied.out: copied.txt + diff --git a/make/tests/makefiles/special_targets/suffixes/suffixes_basic.mk b/make/tests/makefiles/special_targets/suffixes/suffixes_basic.mk new file mode 100644 index 00000000..ca34c145 --- /dev/null +++ b/make/tests/makefiles/special_targets/suffixes/suffixes_basic.mk @@ -0,0 +1,7 @@ +.SUFFIXES: .txt .out + +.txt.out: + @echo "Converting copied.txt to copied.out" + @cp copied.txt copied.out + + diff --git a/make/tests/makefiles/special_targets/validations/without_prerequisites.mk b/make/tests/makefiles/special_targets/validations/without_prerequisites.mk new file mode 100644 index 00000000..75e119b8 --- /dev/null +++ b/make/tests/makefiles/special_targets/validations/without_prerequisites.mk @@ -0,0 +1,5 @@ +.DEFAULT: rule + echo DEFAULT + +rule: + echo rule diff --git a/make/tests/makefiles/special_targets/validations/without_recipes.mk b/make/tests/makefiles/special_targets/validations/without_recipes.mk new file mode 100644 index 00000000..fb85dbe4 --- /dev/null +++ b/make/tests/makefiles/special_targets/validations/without_recipes.mk @@ -0,0 +1,5 @@ +.SILENT: rule + echo silent + +rule: + echo rule diff --git a/make/tests/makefiles/target_behavior/async_events/signal.mk b/make/tests/makefiles/target_behavior/async_events/signal.mk new file mode 100644 index 00000000..ab57ae00 --- /dev/null +++ b/make/tests/makefiles/target_behavior/async_events/signal.mk @@ -0,0 +1,5 @@ +text.txt: + echo "hello" + touch text.txt + sleep 1 + echo "bye" diff --git a/make/tests/makefiles/target_behavior/basic_chaining.mk b/make/tests/makefiles/target_behavior/basic_chaining.mk new file mode 100644 index 00000000..621bfe78 --- /dev/null +++ b/make/tests/makefiles/target_behavior/basic_chaining.mk @@ -0,0 +1,5 @@ +rule1: rule2 + echo rule1 + +rule2: + echo rule2 diff --git a/make/tests/makefiles/target_behavior/diamond_chaining_with_touches/makefile b/make/tests/makefiles/target_behavior/diamond_chaining_with_touches/makefile new file mode 100644 index 00000000..84f5f185 --- /dev/null +++ b/make/tests/makefiles/target_behavior/diamond_chaining_with_touches/makefile @@ -0,0 +1,15 @@ +rule1: rule2 rule3 + echo rule1 + touch rule1 + +rule2: rule4 + echo rule2 + touch rule2 + +rule3: rule4 + echo rule3 + touch rule3 + +rule4: + echo rule4 + touch rule4 diff --git a/make/tests/makefiles/target_behavior/makefile_priority/big_Makefile/Makefile b/make/tests/makefiles/target_behavior/makefile_priority/big_Makefile/Makefile new file mode 100644 index 00000000..af1d51c3 --- /dev/null +++ b/make/tests/makefiles/target_behavior/makefile_priority/big_Makefile/Makefile @@ -0,0 +1,2 @@ +target: + echo Makefile diff --git a/make/tests/makefiles/target_behavior/makefile_priority/little_makefile/makefile b/make/tests/makefiles/target_behavior/makefile_priority/little_makefile/makefile new file mode 100644 index 00000000..a769ad6a --- /dev/null +++ b/make/tests/makefiles/target_behavior/makefile_priority/little_makefile/makefile @@ -0,0 +1,2 @@ +target: + echo makefile diff --git a/make/tests/makefiles/target_behavior/no_targets.mk b/make/tests/makefiles/target_behavior/no_targets.mk new file mode 100644 index 00000000..5c943a4a --- /dev/null +++ b/make/tests/makefiles/target_behavior/no_targets.mk @@ -0,0 +1 @@ +MACRO = value diff --git a/make/tests/makefiles/target_behavior/recursive_chaining.mk b/make/tests/makefiles/target_behavior/recursive_chaining.mk new file mode 100644 index 00000000..ca1beb60 --- /dev/null +++ b/make/tests/makefiles/target_behavior/recursive_chaining.mk @@ -0,0 +1,5 @@ +rule1: rule2 + echo rule1 + +rule2: rule1 + echo rule2 diff --git a/make/tests/parser.rs b/make/tests/parser.rs new file mode 100644 index 00000000..70090c7a --- /dev/null +++ b/make/tests/parser.rs @@ -0,0 +1,446 @@ +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +mod preprocess { + use posixutils_make::parser::preprocessor::preprocess; + + #[test] + fn test_macros_simple() { + const MACROS: &'static str = r#" +VAR = var +V = ok + +all: + $(VAR) $V ${VAR} ${V} $(V) +"#; + + const EXPECTED: &'static str = r#" + +all: + var ok var ok ok +"#; + let Ok(result) = preprocess(MACROS) else { + panic!("Test must be preprocessed without an error") + }; + assert_eq!(result, EXPECTED); + } +} + +mod lex { + use posixutils_make::parser::{lex::lex, SyntaxKind::*}; + + #[test] + fn test_empty() { + assert_eq!(lex(""), vec![]); + } + + #[test] + fn test_simple() { + assert_eq!( + lex(r#"VARIABLE = value + +rule: prerequisite + recipe +"#) + .iter() + .map(|(kind, text)| (*kind, text.as_str())) + .collect::>(), + vec![ + (IDENTIFIER, "VARIABLE"), + (WHITESPACE, " "), + (EQUALS, "="), + (WHITESPACE, " "), + (IDENTIFIER, "value"), + (NEWLINE, "\n"), + (NEWLINE, "\n"), + (IDENTIFIER, "rule"), + (COLON, ":"), + (WHITESPACE, " "), + (IDENTIFIER, "prerequisite"), + (NEWLINE, "\n"), + (INDENT, "\t"), + (TEXT, "recipe"), + (NEWLINE, "\n"), + ] + ); + } + + #[test] + fn test_bare_export() { + assert_eq!( + lex(r#"export +"#) + .iter() + .map(|(kind, text)| (*kind, text.as_str())) + .collect::>(), + vec![(EXPORT, "export"), (NEWLINE, "\n"),] + ); + } + + #[test] + fn test_export() { + assert_eq!( + lex(r#"export VARIABLE +"#) + .iter() + .map(|(kind, text)| (*kind, text.as_str())) + .collect::>(), + vec![ + (EXPORT, "export"), + (WHITESPACE, " "), + (IDENTIFIER, "VARIABLE"), + (NEWLINE, "\n"), + ] + ); + } + + #[test] + fn test_export_assignment() { + assert_eq!( + lex(r#"export VARIABLE := value +"#) + .iter() + .map(|(kind, text)| (*kind, text.as_str())) + .collect::>(), + vec![ + (EXPORT, "export"), + (WHITESPACE, " "), + (IDENTIFIER, "VARIABLE"), + (WHITESPACE, " "), + (COLON, ":"), + (EQUALS, "="), + (WHITESPACE, " "), + (IDENTIFIER, "value"), + (NEWLINE, "\n"), + ] + ); + } + + #[test] + fn test_include() { + assert_eq!( + lex(r#"include FILENAME +"#) + .iter() + .map(|(kind, text)| (*kind, text.as_str())) + .collect::>(), + [ + (INCLUDE, "include"), + (WHITESPACE, " "), + (IDENTIFIER, "FILENAME"), + (NEWLINE, "\n") + ] + ); + } + + #[test] + fn test_multiple_prerequisites() { + assert_eq!( + lex(r#"rule: prerequisite1 prerequisite2 + recipe + +"#) + .iter() + .map(|(kind, text)| (*kind, text.as_str())) + .collect::>(), + vec![ + (IDENTIFIER, "rule"), + (COLON, ":"), + (WHITESPACE, " "), + (IDENTIFIER, "prerequisite1"), + (WHITESPACE, " "), + (IDENTIFIER, "prerequisite2"), + (NEWLINE, "\n"), + (INDENT, "\t"), + (TEXT, "recipe"), + (NEWLINE, "\n"), + (NEWLINE, "\n"), + ] + ); + } + + #[test] + fn test_variable_question() { + assert_eq!( + lex("VARIABLE ?= value\n") + .iter() + .map(|(kind, text)| (*kind, text.as_str())) + .collect::>(), + vec![ + (IDENTIFIER, "VARIABLE"), + (WHITESPACE, " "), + (QUESTION, "?"), + (EQUALS, "="), + (WHITESPACE, " "), + (IDENTIFIER, "value"), + (NEWLINE, "\n"), + ] + ); + } + + #[test] + fn test_conditional() { + assert_eq!( + lex(r#"ifneq (a, b) +endif +"#) + .iter() + .map(|(kind, text)| (*kind, text.as_str())) + .collect::>(), + vec![ + (IDENTIFIER, "ifneq"), + (WHITESPACE, " "), + (LPAREN, "("), + (IDENTIFIER, "a"), + (COMMA, ","), + (WHITESPACE, " "), + (IDENTIFIER, "b"), + (RPAREN, ")"), + (NEWLINE, "\n"), + (IDENTIFIER, "endif"), + (NEWLINE, "\n"), + ] + ); + } + + #[test] + fn test_variable_paren() { + assert_eq!( + lex("VARIABLE = $(value)\n") + .iter() + .map(|(kind, text)| (*kind, text.as_str())) + .collect::>(), + vec![ + (IDENTIFIER, "VARIABLE"), + (WHITESPACE, " "), + (EQUALS, "="), + (WHITESPACE, " "), + (DOLLAR, "$"), + (LPAREN, "("), + (IDENTIFIER, "value"), + (RPAREN, ")"), + (NEWLINE, "\n"), + ] + ); + } + + #[test] + fn test_variable_paren2() { + assert_eq!( + lex("VARIABLE = $(value)$(value2)\n") + .iter() + .map(|(kind, text)| (*kind, text.as_str())) + .collect::>(), + vec![ + (IDENTIFIER, "VARIABLE"), + (WHITESPACE, " "), + (EQUALS, "="), + (WHITESPACE, " "), + (DOLLAR, "$"), + (LPAREN, "("), + (IDENTIFIER, "value"), + (RPAREN, ")"), + (DOLLAR, "$"), + (LPAREN, "("), + (IDENTIFIER, "value2"), + (RPAREN, ")"), + (NEWLINE, "\n"), + ] + ); + } +} + +mod parse { + use posixutils_make::parser::preprocessor::preprocess; + use posixutils_make::parser::{parse::parse, Makefile}; + use rowan::ast::AstNode; + + #[test] + fn test_parse_simple() { + const SIMPLE: &str = r#"VARIABLE = command2 + +rule: dependency + command + ${VARIABLE} + +"#; + let Ok(processed) = preprocess(SIMPLE) else { + panic!("Must be preprocessed without an error") + }; + let parsed = parse(&processed); + println!("{:#?}", parsed.clone().unwrap().syntax()); + assert_eq!(parsed.clone().err(), None); + let node = parsed.clone().unwrap().syntax(); + assert_eq!( + format!("{:#?}", node), + r#"ROOT@0..38 + NEWLINE@0..1 "\n" + RULE@1..38 + IDENTIFIER@1..5 "rule" + COLON@5..6 ":" + WHITESPACE@6..7 " " + EXPR@7..17 + IDENTIFIER@7..17 "dependency" + NEWLINE@17..18 "\n" + RECIPE@18..27 + INDENT@18..19 "\t" + TEXT@19..26 "command" + NEWLINE@26..27 "\n" + RECIPE@27..37 + INDENT@27..28 "\t" + TEXT@28..36 "command2" + NEWLINE@36..37 "\n" + NEWLINE@37..38 "\n" +"# + ); + + let root = parsed.unwrap().root().clone_for_update(); + + let mut rules = root.rules().collect::>(); + assert_eq!(rules.len(), 1); + let rule = rules.pop().unwrap(); + assert_eq!(rule.targets().collect::>(), vec!["rule"]); + assert_eq!(rule.prerequisites().collect::>(), vec!["dependency"]); + assert_eq!( + rule.recipes().collect::>(), + vec!["command", "command2"] + ); + } + + #[test] + fn test_parse_export_assign() { + const EXPORT: &str = r#"export VARIABLE := value +"#; + let Ok(processed) = preprocess(EXPORT).map_err(|e| println!("{e:?}")) else { + panic!("Must be preprocessed without an error") + }; + let parsed = parse(&processed); + assert!(parsed.clone().err().is_some()); + } + + // TODO: create `include` test with real files + // + // #[test] + // fn test_parse_include() { + // const INCLUDE: &str = r#"include FILENAME + // "#; + // let Ok(processed) = preprocess(INCLUDE) else { panic!("Could not preprocess") }; + // let parsed = parse(&processed); + // assert_eq!(parsed.errors, Vec::::new()); + // let node = parsed.syntax(); + // + // assert_eq!( + // format!("{:#?}", node), + // r#"ROOT@0..17 + // IDENTIFIER@0..7 "include" + // WHITESPACE@7..8 " " + // IDENTIFIER@8..16 "FILENAME" + // NEWLINE@16..17 "\n" + // "# + // ); + // + // let root = parsed.root().clone_for_update(); + // + // let variables = root.syntax(); + // dbg!(&variables); + // // assert_eq!(variables.len(), 1); + // // let variable = variables.pop().unwrap(); + // // assert_eq!(variable.name(), Some("VARIABLE".to_string())); + // // assert_eq!(variable.raw_value(), Some("value".to_string())); + // } + + #[test] + fn test_parse_multiple_prerequisites() { + const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2 + command + +"#; + let parsed = parse(MULTIPLE_PREREQUISITES); + assert_eq!(parsed.clone().err(), None); + let node = parsed.clone().unwrap().syntax(); + assert_eq!( + format!("{:#?}", node), + r#"ROOT@0..40 + RULE@0..40 + IDENTIFIER@0..4 "rule" + COLON@4..5 ":" + WHITESPACE@5..6 " " + EXPR@6..29 + IDENTIFIER@6..17 "dependency1" + WHITESPACE@17..18 " " + IDENTIFIER@18..29 "dependency2" + NEWLINE@29..30 "\n" + RECIPE@30..39 + INDENT@30..31 "\t" + TEXT@31..38 "command" + NEWLINE@38..39 "\n" + NEWLINE@39..40 "\n" +"# + ); + let root = parsed.unwrap().root().clone_for_update(); + + let rule = root.rules().next().unwrap(); + assert_eq!(rule.targets().collect::>(), vec!["rule"]); + assert_eq!( + rule.prerequisites().collect::>(), + vec!["dependency1", "dependency2"] + ); + assert_eq!(rule.recipes().collect::>(), vec!["command"]); + } + + #[test] + fn test_add_rule() { + let mut makefile = Makefile::new(); + let rule = makefile.add_rule("rule"); + assert_eq!(rule.targets().collect::>(), vec!["rule"]); + assert_eq!( + rule.prerequisites().collect::>(), + Vec::::new() + ); + + assert_eq!(makefile.to_string(), "rule:\n"); + } + + #[test] + fn test_push_command() { + let mut makefile = Makefile::new(); + let rule = makefile.add_rule("rule"); + rule.push_command("command"); + assert_eq!(rule.recipes().collect::>(), vec!["command"]); + + assert_eq!(makefile.to_string(), "rule:\n\tcommand\n"); + + rule.push_command("command2"); + assert_eq!( + rule.recipes().collect::>(), + vec!["command", "command2"] + ); + + assert_eq!(makefile.to_string(), "rule:\n\tcommand\n\tcommand2\n"); + } + + #[test] + fn test_replace_command() { + let mut makefile = Makefile::new(); + let rule = makefile.add_rule("rule"); + rule.push_command("command"); + rule.push_command("command2"); + assert_eq!( + rule.recipes().collect::>(), + vec!["command", "command2"] + ); + + rule.replace_command(0, "new command"); + assert_eq!( + rule.recipes().collect::>(), + vec!["new command", "command2"] + ); + + assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n"); + } +}