diff --git a/bin/dune_init.ml b/bin/dune_init.ml index b1d5be32ab7..14ffb92129b 100644 --- a/bin/dune_init.ml +++ b/bin/dune_init.ml @@ -434,6 +434,7 @@ module Component = struct } ] ~contents_basename:None + ~enabled_if:None in let packages = Package.Name.Map.singleton (Package.name package) package in let info = diff --git a/doc/changes/unreleased/12905.md b/doc/changes/unreleased/12905.md new file mode 100644 index 00000000000..664e9286478 --- /dev/null +++ b/doc/changes/unreleased/12905.md @@ -0,0 +1,4 @@ +- Add `enabled_if` on the `package` stanza in `dune-project` files. If this field is set, + the expression is evaluated to determine whether to mask the package in this + workspace. When opam file generation is enabled, this field is converted to + a corresponding `available` expression (#12905, @rgrinbreg, @gridbugs) diff --git a/src/dune_lang/dune_project.ml b/src/dune_lang/dune_project.ml index 8e82883761b..78d2ff677f0 100644 --- a/src/dune_lang/dune_project.ml +++ b/src/dune_lang/dune_project.ml @@ -883,199 +883,205 @@ let parse_packages let parse ~dir ~(lang : Lang.Instance.t) ~file = let open Decoder in + fields + @@ + let* explicit_extensions = + multi_field + "using" + (let+ loc = loc + and+ name = located string + and+ ver = located Syntax.Version.decode + and+ parse_args = capture in + (* We don't parse the arguments quite yet as we want to set the + version of extensions before parsing them. *) + Extension.instantiate ~dune_lang_ver:lang.version ~loc ~parse_args name ver) + in String_with_vars.set_decoding_env - (Pform.Env.initial ~stanza:lang.version ~extensions:[]) - @@ fields - @@ let+ name = field_o "name" Dune_project_name.decode - and+ version = field_o "version" Package_version.decode - and+ info = Package_info.decode () - and+ packages = multi_field "package" (Package.decode ~dir) - and+ pins = Pin_stanza.Project.decode - and+ explicit_extensions = - multi_field - "using" - (let+ loc = loc - and+ name = located string - and+ ver = located Syntax.Version.decode - and+ parse_args = capture in - (* We don't parse the arguments quite yet as we want to set the - version of extensions before parsing them. *) - Extension.instantiate ~dune_lang_ver:lang.version ~loc ~parse_args name ver) - and+ implicit_transitive_deps = - field_o "implicit_transitive_deps" Implicit_transitive_deps.Stanza.decode - and+ wrapped_executables = - field_o_b "wrapped_executables" ~check:(Syntax.since Stanza.syntax (1, 11)) - and+ map_workspace_root = - field_o_b "map_workspace_root" ~check:(Syntax.since Stanza.syntax (3, 7)) - and+ allow_approximate_merlin = - (* TODO DUNE4 remove this field from parsing *) - let+ loc = loc - and+ field = - field_b "allow_approximate_merlin" ~check:(Syntax.since Stanza.syntax (1, 9)) - in - Option.some_if field loc - and+ executables_implicit_empty_intf = - field_o_b - "executables_implicit_empty_intf" - ~check:(Syntax.since Stanza.syntax (2, 9)) - and+ accept_alternative_dune_file_name = - field_b - "accept_alternative_dune_file_name" - ~check:(Syntax.since Stanza.syntax (3, 0)) - and+ () = Versioned_file.no_more_lang - and+ generate_opam_files = - field_o_b "generate_opam_files" ~check:(Syntax.since Stanza.syntax (1, 10)) - and+ use_standard_c_and_cxx_flags = - field_o_b "use_standard_c_and_cxx_flags" ~check:(Syntax.since Stanza.syntax (2, 8)) - and+ dialects = - multi_field - "dialect" - (Syntax.since Stanza.syntax (1, 11) >>> located Dialect.decode) - and+ explicit_js_mode = - field_o_b "explicit_js_mode" ~check:(Syntax.since Stanza.syntax (1, 11)) - and+ format_config = Format_config.field ~since:(2, 0) - and+ subst_config = Subst_config.field - and+ strict_package_deps = - field_o_b "strict_package_deps" ~check:(Syntax.since Stanza.syntax (2, 3)) - and+ cram = Toggle.field "cram" ~check:(Syntax.since Stanza.syntax (2, 7)) - and+ expand_aliases_in_sandbox = - field_o_b "expand_aliases_in_sandbox" ~check:(Syntax.since Stanza.syntax (3, 0)) - and+ opam_file_location = - field_o - "opam_file_location" - (Syntax.since Stanza.syntax (3, 8) - >>> enum - [ "relative_to_project", `Relative_to_project - ; "inside_opam_directory", `Inside_opam_directory - ]) - and+ warnings = - field - "warnings" - ~default:Warning.Settings.empty - (Syntax.since Stanza.syntax (3, 11) >>> Warning.Settings.decode) - and+ () = - field_o - "pkg" - (let+ loc = loc - and+ () = junk_everything in - loc) - >>| Option.iter ~f:(fun loc -> - User_error.raise - ~loc - ~hints: - [ Pp.text - "Move this stanza to your dune-workspace file. If you don't have one, \ - create one in your workspace root." - ] - [ Pp.text "The (pkg ...) stanza is only valid in dune-workspace files." ]) + (let extensions = + List.map explicit_extensions ~f:(fun (instance : Extension.instance) -> + let (Extension.Packed { syntax; _ }) = instance.extension in + syntax, instance.version) in - fun (opam_packages : (Loc.t * Package.t Memo.t) Package.Name.Map.t) -> - let opam_file_location = - Option.value opam_file_location ~default:(opam_file_location_default ~lang) - in - let generate_opam_files = Option.value ~default:false generate_opam_files in - let open Memo.O in - let+ packages = - parse_packages - name - ~info - ~dir - ~version - packages - opam_file_location - ~generate_opam_files - opam_packages - in - let name = - match name with - | Some n -> n - | None -> default_name ~dir ~packages - in - let explicit_extensions = explicit_extensions_map explicit_extensions in - let parsing_context, stanza_parser, extension_args = - interpret_lang_and_extensions ~lang ~explicit_extensions - in - let implicit_transitive_deps = - Option.value - implicit_transitive_deps - ~default:(Implicit_transitive_deps.Stanza.default ~lang) - in - let wrapped_executables = - Option.value wrapped_executables ~default:(wrapped_executables_default ~lang) - in - let map_workspace_root = - Option.value map_workspace_root ~default:(map_workspace_root_default ~lang) - in - let executables_implicit_empty_intf = - Option.value - executables_implicit_empty_intf - ~default:(executables_implicit_empty_intf_default ~lang) - in - let strict_package_deps = - Option.value strict_package_deps ~default:(strict_package_deps_default ~lang) - in - let dune_version = lang.version in - let explicit_js_mode = - Option.value explicit_js_mode ~default:(explicit_js_mode_default ~lang) - in - let use_standard_c_and_cxx_flags = - match use_standard_c_and_cxx_flags with - | None -> use_standard_c_and_cxx_flags_default ~lang - | some -> some - in - let cram = - match cram with - | None -> cram_default ~lang - | Some t -> Toggle.enabled t - in - let expand_aliases_in_sandbox = - Option.value - expand_aliases_in_sandbox - ~default:(expand_aliases_in_sandbox_default ~lang) - in - let root = dir in - let dialects = - let dialects = - match String.Map.find explicit_extensions Melange_syntax.name with - | Some extension -> (extension.loc, Dialect.rescript) :: dialects - | None -> dialects - in - List.fold_left - dialects - ~init:Dialect.DB.builtin - ~f:(fun dialects (loc, dialect) -> Dialect.DB.add dialects ~loc dialect) - in - { name - ; root - ; version - ; dune_version - ; info - ; packages - ; stanza_parser - ; project_file = Some file - ; extension_args - ; parsing_context - ; implicit_transitive_deps - ; wrapped_executables - ; map_workspace_root - ; executables_implicit_empty_intf - ; accept_alternative_dune_file_name - ; generate_opam_files - ; warnings - ; use_standard_c_and_cxx_flags - ; dialects - ; explicit_js_mode - ; format_config - ; subst_config - ; strict_package_deps - ; allow_approximate_merlin - ; pins - ; cram - ; expand_aliases_in_sandbox - ; opam_file_location - ; including_hidden_packages = packages - ; exclusive_dir_packages = make_exclusive_dir_packages packages - } + Pform.Env.initial ~stanza:lang.version ~extensions) + @@ + let parsing_context, _, _ = + let explicit_extensions = explicit_extensions_map explicit_extensions in + interpret_lang_and_extensions ~lang ~explicit_extensions + in + Decoder.set_many parsing_context + @@ + let+ name = field_o "name" Dune_project_name.decode + and+ version = field_o "version" Package_version.decode + and+ info = Package_info.decode () + and+ packages = multi_field "package" (Package.decode ~dir) + and+ pins = Pin_stanza.Project.decode + and+ implicit_transitive_deps = + field_o "implicit_transitive_deps" Implicit_transitive_deps.Stanza.decode + and+ wrapped_executables = + field_o_b "wrapped_executables" ~check:(Syntax.since Stanza.syntax (1, 11)) + and+ map_workspace_root = + field_o_b "map_workspace_root" ~check:(Syntax.since Stanza.syntax (3, 7)) + and+ allow_approximate_merlin = + (* TODO DUNE4 remove this field from parsing *) + let+ loc = loc + and+ field = + field_b "allow_approximate_merlin" ~check:(Syntax.since Stanza.syntax (1, 9)) + in + Option.some_if field loc + and+ executables_implicit_empty_intf = + field_o_b "executables_implicit_empty_intf" ~check:(Syntax.since Stanza.syntax (2, 9)) + and+ accept_alternative_dune_file_name = + field_b "accept_alternative_dune_file_name" ~check:(Syntax.since Stanza.syntax (3, 0)) + and+ () = Versioned_file.no_more_lang + and+ generate_opam_files = + field_o_b "generate_opam_files" ~check:(Syntax.since Stanza.syntax (1, 10)) + and+ use_standard_c_and_cxx_flags = + field_o_b "use_standard_c_and_cxx_flags" ~check:(Syntax.since Stanza.syntax (2, 8)) + and+ dialects = + multi_field "dialect" (Syntax.since Stanza.syntax (1, 11) >>> located Dialect.decode) + and+ explicit_js_mode = + field_o_b "explicit_js_mode" ~check:(Syntax.since Stanza.syntax (1, 11)) + and+ format_config = Format_config.field ~since:(2, 0) + and+ subst_config = Subst_config.field + and+ strict_package_deps = + field_o_b "strict_package_deps" ~check:(Syntax.since Stanza.syntax (2, 3)) + and+ cram = Toggle.field "cram" ~check:(Syntax.since Stanza.syntax (2, 7)) + and+ expand_aliases_in_sandbox = + field_o_b "expand_aliases_in_sandbox" ~check:(Syntax.since Stanza.syntax (3, 0)) + and+ opam_file_location = + field_o + "opam_file_location" + (Syntax.since Stanza.syntax (3, 8) + >>> enum + [ "relative_to_project", `Relative_to_project + ; "inside_opam_directory", `Inside_opam_directory + ]) + and+ warnings = + field + "warnings" + ~default:Warning.Settings.empty + (Syntax.since Stanza.syntax (3, 11) >>> Warning.Settings.decode) + and+ () = + field_o + "pkg" + (let+ loc = loc + and+ () = junk_everything in + loc) + >>| Option.iter ~f:(fun loc -> + User_error.raise + ~loc + ~hints: + [ Pp.text + "Move this stanza to your dune-workspace file. If you don't have one, \ + create one in your workspace root." + ] + [ Pp.text "The (pkg ...) stanza is only valid in dune-workspace files." ]) + in + fun (opam_packages : (Loc.t * Package.t Memo.t) Package.Name.Map.t) -> + let opam_file_location = + Option.value opam_file_location ~default:(opam_file_location_default ~lang) + in + let generate_opam_files = Option.value ~default:false generate_opam_files in + let open Memo.O in + let+ packages = + parse_packages + name + ~info + ~dir + ~version + packages + opam_file_location + ~generate_opam_files + opam_packages + in + let name = + match name with + | Some n -> n + | None -> default_name ~dir ~packages + in + let explicit_extensions = explicit_extensions_map explicit_extensions in + let parsing_context, stanza_parser, extension_args = + interpret_lang_and_extensions ~lang ~explicit_extensions + in + let implicit_transitive_deps = + Option.value + implicit_transitive_deps + ~default:(Implicit_transitive_deps.Stanza.default ~lang) + in + let wrapped_executables = + Option.value wrapped_executables ~default:(wrapped_executables_default ~lang) + in + let map_workspace_root = + Option.value map_workspace_root ~default:(map_workspace_root_default ~lang) + in + let executables_implicit_empty_intf = + Option.value + executables_implicit_empty_intf + ~default:(executables_implicit_empty_intf_default ~lang) + in + let strict_package_deps = + Option.value strict_package_deps ~default:(strict_package_deps_default ~lang) + in + let dune_version = lang.version in + let explicit_js_mode = + Option.value explicit_js_mode ~default:(explicit_js_mode_default ~lang) + in + let use_standard_c_and_cxx_flags = + match use_standard_c_and_cxx_flags with + | None -> use_standard_c_and_cxx_flags_default ~lang + | some -> some + in + let cram = + match cram with + | None -> cram_default ~lang + | Some t -> Toggle.enabled t + in + let expand_aliases_in_sandbox = + Option.value + expand_aliases_in_sandbox + ~default:(expand_aliases_in_sandbox_default ~lang) + in + let root = dir in + let dialects = + let dialects = + match String.Map.find explicit_extensions Melange_syntax.name with + | Some extension -> (extension.loc, Dialect.rescript) :: dialects + | None -> dialects + in + List.fold_left dialects ~init:Dialect.DB.builtin ~f:(fun dialects (loc, dialect) -> + Dialect.DB.add dialects ~loc dialect) + in + { name + ; root + ; version + ; dune_version + ; info + ; packages + ; stanza_parser + ; project_file = Some file + ; extension_args + ; parsing_context + ; implicit_transitive_deps + ; wrapped_executables + ; map_workspace_root + ; executables_implicit_empty_intf + ; accept_alternative_dune_file_name + ; generate_opam_files + ; warnings + ; use_standard_c_and_cxx_flags + ; dialects + ; explicit_js_mode + ; format_config + ; subst_config + ; strict_package_deps + ; allow_approximate_merlin + ; pins + ; cram + ; expand_aliases_in_sandbox + ; opam_file_location + ; including_hidden_packages = packages + ; exclusive_dir_packages = make_exclusive_dir_packages packages + } ;; let load_dune_project ~read ~dir opam_packages : t Memo.t = diff --git a/src/dune_lang/package.ml b/src/dune_lang/package.ml index 9b2c062c786..919c3b1ea9c 100644 --- a/src/dune_lang/package.ml +++ b/src/dune_lang/package.ml @@ -30,6 +30,7 @@ type t = ; depends : Package_dependency.t list ; conflicts : Package_dependency.t list ; depopts : Package_dependency.t list + ; enabled_if : Blang.t option ; info : Package_info.t ; version : Package_version.t option ; has_opam_file : opam_file @@ -65,6 +66,7 @@ let set_inside_opam_dir t ~dir = { t with opam_file = Name.file t.id.name ~dir } let set_version_and_info t ~version ~info = { t with version; info } let exclusive_dir t = t.exclusive_dir let duplicate_dep_warnings t = t.duplicate_dep_warnings +let enabled_if t = t.enabled_if let encode (name : Name.t) @@ -86,6 +88,7 @@ let encode ; original_opam_file = _ ; exclusive_dir ; duplicate_dep_warnings = _ + ; enabled_if } = let open Encoder in @@ -98,6 +101,7 @@ let encode ; field_l "depends" Package_dependency.encode depends ; field_l "conflicts" Package_dependency.encode conflicts ; field_l "depopts" Package_dependency.encode depopts + ; field_o "enabled_if" Blang.encode enabled_if ; field_o "version" Package_version.encode version ; field "tags" (list string) ~default:[] tags ; field_l @@ -120,6 +124,11 @@ let decode_name ~version = ;; let decode = + let enabled_if = + String_with_vars.set_decoding_env + Pform.Env.package_enabled_if + (Blang.Ast.decode ~override_decode_bare_literal:None String_with_vars.decode) + in let open Decoder in let name_map syntax of_list_map to_string name decode print_value error_msg = let+ names = field ~default:[] name (syntax >>> repeat decode) in @@ -166,6 +175,7 @@ let decode = field ~default:[] "conflicts" (repeat (located Package_dependency.decode)) and+ depopts_with_locs = field ~default:[] "depopts" (repeat (located Package_dependency.decode)) + and+ enabled_if = field_o "enabled_if" (Unreleased.since () >>> enabled_if) and+ info = Package_info.decode ~since:(2, 0) () and+ tags = field "tags" (enter (repeat string)) ~default:[] and+ exclusive_dir = @@ -224,6 +234,7 @@ let decode = ; original_opam_file = None ; exclusive_dir ; duplicate_dep_warnings + ; enabled_if } ;; @@ -253,6 +264,7 @@ let to_dyn ; original_opam_file = _ ; exclusive_dir = _ ; duplicate_dep_warnings = _ + ; enabled_if } = let open Dyn in @@ -270,6 +282,7 @@ let to_dyn ; "deprecated_package_names", Name.Map.to_dyn Loc.to_dyn_hum deprecated_package_names ; "sites", Site.Map.to_dyn Section.to_dyn sites ; "allow_empty", Bool allow_empty + ; "enabled_if", (option Blang.to_dyn) enabled_if ] ;; @@ -286,6 +299,7 @@ let create ~conflicts ~depends ~depopts + ~enabled_if ~info ~has_opam_file ~dir @@ -318,5 +332,6 @@ let create ; exclusive_dir = Option.map contents_basename ~f:(fun (loc, s) -> loc, Path.Source.relative dir s) ; duplicate_dep_warnings = [] + ; enabled_if } ;; diff --git a/src/dune_lang/package.mli b/src/dune_lang/package.mli index a2d65841453..52629d802ee 100644 --- a/src/dune_lang/package.mli +++ b/src/dune_lang/package.mli @@ -36,6 +36,7 @@ val tags : t -> string list val synopsis : t -> string option val info : t -> Package_info.t val description : t -> string option +val enabled_if : t -> Blang.t option val id : t -> Id.t val set_version_and_info @@ -60,6 +61,7 @@ val create -> conflicts:Package_dependency.t list -> depends:Package_dependency.t list -> depopts:Package_dependency.t list + -> enabled_if:Blang.t option -> info:Package_info.t -> has_opam_file:opam_file -> dir:Path.Source.t diff --git a/src/dune_lang/package_constraint.ml b/src/dune_lang/package_constraint.ml index cd61b5c25dd..be46fe86dbe 100644 --- a/src/dune_lang/package_constraint.ml +++ b/src/dune_lang/package_constraint.ml @@ -34,7 +34,7 @@ end module T = struct type t = - | Bvar of Package_variable_name.t + | Bvar of Value.t | Uop of Relop.t * Value.t | Bop of Relop.t * Value.t * Value.t | And of t list @@ -44,7 +44,7 @@ module T = struct let rec to_dyn = let open Dyn in function - | Bvar v -> variant "Bvar" [ Package_variable_name.to_dyn v ] + | Bvar v -> variant "Bvar" [ Value.to_dyn v ] | Uop (b, x) -> variant "Uop" [ Relop.to_dyn b; Value.to_dyn x ] | Bop (b, x, y) -> variant "Bop" [ Relop.to_dyn b; Value.to_dyn x; Value.to_dyn y ] | And t -> variant "And" (List.map ~f:to_dyn t) @@ -55,7 +55,7 @@ module T = struct let rec compare a b = let open Ordering.O in match a, b with - | Bvar a, Bvar b -> Package_variable_name.compare a b + | Bvar a, Bvar b -> Value.compare a b | Bvar _, _ -> Lt | _, Bvar _ -> Gt | Uop (a_op, a_value), Uop (b_op, b_value) -> @@ -85,7 +85,12 @@ include Comparable.Make (T) let rec encode c = let open Encoder in match c with - | Bvar x -> Package_variable_name.Project.encode x + | Bvar (String_literal _) -> + (* We don't need to encode such values at the moment. They can only be + constructed when converting from [enabled_if] to [available]. Our sexp + decoder never produces such values for example. *) + assert false + | Bvar (Variable x) -> Package_variable_name.Project.encode x | Uop (op, x) -> pair Relop.encode Value.encode (op, x) | Bop (op, x, y) -> triple Relop.encode Value.encode Value.encode (op, x, y) | And conjuncts -> list sexp (string "and" :: List.map ~f:encode conjuncts) @@ -154,7 +159,7 @@ let decode = >>= function | Atom (_loc, A s) when String.is_prefix s ~prefix:":" -> let+ () = junk in - Bvar (Package_variable_name.of_string (String.drop s 1)) + Bvar (Variable (Package_variable_name.of_string (String.drop s 1))) | _ -> sum (ops @ logops)) ;; diff --git a/src/dune_lang/package_constraint.mli b/src/dune_lang/package_constraint.mli index db427316e4c..7a2d31b4024 100644 --- a/src/dune_lang/package_constraint.mli +++ b/src/dune_lang/package_constraint.mli @@ -13,7 +13,7 @@ end represent strings and booleans. If a variable appears in a position where a boolean is expected it will be assumed to represent a boolean. *) type t = - | Bvar of Package_variable_name.t (** A boolean variable *) + | Bvar of Value.t | Uop of Relop.t * Value.t (** A unary operator applied to a value. Unary operators are operators whose LHS is implied by context. E.g. when placing version constraints diff --git a/src/dune_lang/package_dependency.ml b/src/dune_lang/package_dependency.ml index 99de042b6e5..94134a71243 100644 --- a/src/dune_lang/package_dependency.ml +++ b/src/dune_lang/package_dependency.ml @@ -61,7 +61,8 @@ let check_for_typo ~loc { name; constraint_ } = ] in Some message - | Some (Bvar var) when String.equal (Package_variable_name.to_string var) "with_test" -> + | Some (Bvar (Variable var)) + when String.equal (Package_variable_name.to_string var) "with_test" -> let message = User_message.make ~loc diff --git a/src/dune_lang/pform.ml b/src/dune_lang/pform.ml index 12499a1c182..2e0ffce4aee 100644 --- a/src/dune_lang/pform.ml +++ b/src/dune_lang/pform.ml @@ -649,6 +649,10 @@ module Env = struct } ;; + let os ?what ~version () = + List.map Var.Os.all ~f:(fun v -> Var.Os.to_string v, since ?what ~version (Var.Os v)) + ;; + let initial = let macros = let macro (x : Macro.t) = No_info x in @@ -766,11 +770,8 @@ module Env = struct ; "dune-warnings", since ~version:(3, 21) Var.Dune_warnings ] in - let os = - List.map Var.Os.all ~f:(fun v -> - Var.Os.to_string v, since ~version:(3, 20) (Var.Os v)) - in - String.Map.of_list_exn (List.concat [ lowercased; uppercased; other; os ]) + String.Map.of_list_exn + (List.concat [ lowercased; uppercased; other; os ~version:(3, 20) () ]) in fun ~stanza:syntax_version ~extensions -> let extensions = @@ -779,6 +780,22 @@ module Env = struct { syntax_version; syntax_lang = Stanza.syntax; vars; macros; extensions } ;; + let package_enabled_if = + let syntax_version = 3, 21 in + let syntax_lang = Unreleased.syntax in + let vars = + let os = os ~what:syntax_lang ~version:syntax_version () in + (* CR rgrinberg: This has to be disabled for multi context builds *) + ("architecture", No_info Var.Architecture) :: os + in + { syntax_version = 0, 1 + ; syntax_lang = Unreleased.syntax + ; vars = String.Map.of_list_exn vars + ; macros = String.Map.empty + ; extensions = Syntax.Map.singleton Unreleased.syntax syntax_version + } + ;; + let lt_renamed_input_file t = { t with vars = diff --git a/src/dune_lang/pform.mli b/src/dune_lang/pform.mli index 8413326c276..a27f93c975b 100644 --- a/src/dune_lang/pform.mli +++ b/src/dune_lang/pform.mli @@ -204,6 +204,7 @@ module Env : sig -> extensions:(Syntax.t * Syntax.Version.t) list -> t + val package_enabled_if : t val add_user_vars : t -> string list -> t val parse : t -> Template.Pform.t -> pform diff --git a/src/dune_pkg/opam_file.ml b/src/dune_pkg/opam_file.ml index 6a06d79b87a..fb64eb37535 100644 --- a/src/dune_pkg/opam_file.ml +++ b/src/dune_pkg/opam_file.ml @@ -282,6 +282,7 @@ let load_opam_file_with_contents ~contents:opam_file_string file name = ~conflicts:[] ~depends:[] ~depopts:[] + ~enabled_if:None (* CR-someday rgrinberg: would be nice to interpret this *) ~info ~synopsis:(get_one "synopsis") ~description:(get_one "description") diff --git a/src/dune_pkg/package_dependency.ml b/src/dune_pkg/package_dependency.ml index fb9746684ae..8e0886c6fcb 100644 --- a/src/dune_pkg/package_dependency.ml +++ b/src/dune_pkg/package_dependency.ml @@ -73,8 +73,9 @@ module Constraint = struct end let rec to_opam_condition = function - | Bvar variable -> + | Bvar (Variable variable) -> OpamTypes.Atom (OpamTypes.Filter (Variable.to_opam_filter variable)) + | Bvar (String_literal s) -> OpamTypes.Atom (OpamTypes.Filter (FString s)) | Uop (op, value) -> OpamTypes.Atom (OpamTypes.Constraint (Op.to_relop_pelem op, Value.to_opam_filter value)) @@ -96,7 +97,7 @@ module Constraint = struct let rec of_opam_filter (filter : OpamTypes.filter) = let open Result.O in match filter with - | FIdent ([], name, None) -> Ok (Bvar (Variable.of_opam name)) + | FIdent ([], name, None) -> Ok (Bvar (Variable (Variable.of_opam name))) | FOp (lhs, relop, rhs) -> let op = Op.of_opam relop in let+ lhs = Value.of_opam_filter lhs @@ -113,6 +114,7 @@ module Constraint = struct | FNot constraint_ -> let+ constraint_ = of_opam_filter constraint_ in Not constraint_ + (* CR-someday rgrinberg: Handle FString? *) | _ -> Error (Convert_from_opam_error.Can't_convert_opam_filter_to_condition filter) ;; @@ -166,7 +168,8 @@ let op_list op = function let opam_constraint t : OpamParserTypes.FullPos.value = let open OpamParserTypes.FullPos in let rec opam_constraint context = function - | Constraint.Bvar v -> Constraint.Variable.to_opam v + | Constraint.Bvar (Variable v) -> Constraint.Variable.to_opam v + | Constraint.Bvar (String_literal v) -> nopos (String v) | Uop (op, x) -> nopos (Prefix_relop (nopos @@ Constraint.Op.to_opam op, Constraint.Value.to_opam x)) | Bop (op, x, y) -> diff --git a/src/dune_pkg/package_dependency.mli b/src/dune_pkg/package_dependency.mli index 352bb26b658..ff6d7085795 100644 --- a/src/dune_pkg/package_dependency.mli +++ b/src/dune_pkg/package_dependency.mli @@ -24,6 +24,7 @@ type t = Dune_lang.Package_dependency.t = include module type of Dune_lang.Package_dependency with type t := t +val opam_constraint : Constraint.t -> OpamParserTypes.FullPos.value val opam_depend : t -> OpamParserTypes.FullPos.value val to_opam_filtered_formula : t -> OpamTypes.filtered_formula diff --git a/src/dune_rules/dune_load.ml b/src/dune_rules/dune_load.ml index 830c00e7c2c..302344cf439 100644 --- a/src/dune_rules/dune_load.ml +++ b/src/dune_rules/dune_load.ml @@ -47,7 +47,7 @@ let load () = | Vendored -> `Vendored | Normal | Data_only -> `Regular in - let+ projects, dune_files = + let* projects, dune_files = let f dir : Projects_and_dune_files.t Memo.t = let path = Source_tree.Dir.path dir in let project = Source_tree.Dir.project dir in @@ -69,12 +69,40 @@ let load () = ~f in let projects = Appendable_list.to_list_rev projects in - let all_packages, vendored_packages = - List.fold_left + let+ all_packages, vendored_packages = + Memo.List.fold_left projects ~init:(Package.Name.Map.empty, Package.Name.Set.empty) ~f:(fun (acc_packages, vendored) (status, (project : Dune_project.t)) -> - let packages = Dune_project.including_hidden_packages project in + let+ packages = + let packages = Dune_project.including_hidden_packages project in + let+ disabled = + Package.Name.Map.values packages + |> List.filter_map ~f:(fun package -> + Package.enabled_if package |> Option.map ~f:(fun expr -> package, expr)) + |> Memo.List.map ~f:(fun (package, expr) -> + Blang_expand.eval + expr + ~dir:Path.root (* This value is irrelevant *) + ~f:(fun ~source:_ pform -> + match pform with + | Var (Os v) -> Lock_dir.Sys_vars.(os_values poll v) + | Var Architecture -> + let+ arch = Memo.Lazy.force Lock_dir.Sys_vars.poll.arch in + [ Value.String (Option.value ~default:"" arch) ] + | _ -> assert false) + >>| function + | true -> None + | false -> Some package) + >>| List.filter_opt + >>| Package.Name.Map.of_list_map_exn ~f:(fun pkg -> Package.name pkg, ()) + in + Package.Name.Map.merge packages disabled ~f:(fun _key package disabled -> + match package, disabled with + | Some p, Some () -> Some (p, `Disabled) + | Some p, None -> Some (p, `Enabled) + | None, None | None, Some _ -> assert false) + in let vendored = match status with | `Regular -> vendored @@ -82,7 +110,7 @@ let load () = Package.Name.Set.of_keys packages |> Package.Name.Set.union vendored in let acc_packages = - Package.Name.Map.union acc_packages packages ~f:(fun name a b -> + Package.Name.Map.union acc_packages packages ~f:(fun name (a, _) (b, _) -> User_error.raise [ Pp.textf "The package %S is defined more than once:" @@ -94,7 +122,9 @@ let load () = acc_packages, vendored) in let mask = Only_packages.mask all_packages ~vendored:vendored_packages in - let packages = Only_packages.filter_packages mask all_packages in + let packages = + Package.Name.Map.map ~f:fst all_packages |> Only_packages.filter_packages mask + in let projects = List.rev_map projects ~f:snd in let dune_files = let without_ctx = @@ -102,7 +132,7 @@ let load () = let (_ : Package.Name.t Path.Source.Map.t) = match Package.Name.Map.values all_packages - |> List.filter_map ~f:(fun pkg -> + |> List.filter_map ~f:(fun (pkg, _) -> match Package.exclusive_dir pkg with | None -> None | Some d -> Some (d, pkg)) diff --git a/src/dune_rules/expander.ml b/src/dune_rules/expander.ml index b66902c309e..449931f2ee5 100644 --- a/src/dune_rules/expander.ml +++ b/src/dune_rules/expander.ml @@ -522,10 +522,7 @@ let expand_pform_var (context : Context.t) ~dir ~source (var : Pform.Var.t) = (let+ ocaml = ocaml in lib_config_var var ocaml.lib_config) |> static - | Os v -> - static - (let+ v = Lock_dir.Sys_vars.(os poll v) in - [ Value.String (Option.value v ~default:"") ]) + | Os v -> static Lock_dir.Sys_vars.(os_values poll v) | Ext_exe | Cpp | Pa_cpp diff --git a/src/dune_rules/lock_dir.ml b/src/dune_rules/lock_dir.ml index bffcba4ab6c..461613e81d1 100644 --- a/src/dune_rules/lock_dir.ml +++ b/src/dune_rules/lock_dir.ml @@ -42,6 +42,11 @@ module Sys_vars = struct } ;; + let os_values t v = + let+ v = os t v in + [ Value.String (Option.value v ~default:"") ] + ;; + (* A pform expander for expanding a subset of the variables in "lang dune" (ie. the same variables available in dune files) based on the OPAM variables polled by this module. OPAM variables are converted to their equivalent dune diff --git a/src/dune_rules/lock_dir.mli b/src/dune_rules/lock_dir.mli index dce38dc9a5a..4f60adf5847 100644 --- a/src/dune_rules/lock_dir.mli +++ b/src/dune_rules/lock_dir.mli @@ -43,6 +43,7 @@ module Sys_vars : sig } val os : t -> Dune_lang.Pform.Var.Os.t -> string option Memo.t + val os_values : t -> Dune_lang.Pform.Var.Os.t -> Value.t list Memo.t val poll : t val solver_env : Dune_pkg.Solver_env.t Memo.t end diff --git a/src/dune_rules/opam_create.ml b/src/dune_rules/opam_create.ml index 8f9205326fc..7b9ec4621e6 100644 --- a/src/dune_rules/opam_create.ml +++ b/src/dune_rules/opam_create.ml @@ -108,6 +108,33 @@ let default_build_command = ~with_sites:Dune_project.(is_extension_set project dune_site_extension)) ;; +let var_of_sw sw : Package_constraint.Value.t = + match String_with_vars.pform_only sw with + | None -> + (match String_with_vars.text_only sw with + | Some s -> String_literal s + | None -> assert false) + | Some s -> + Variable + (match s with + | Var Architecture -> Package_variable_name.arch + | Var (Os Os) -> Package_variable_name.os + | Var (Os Os_version) -> Package_variable_name.os_version + | Var (Os Os_distribution) -> Package_variable_name.os_distribution + | Var (Os Os_family) -> Package_variable_name.os_family + | _ -> assert false) +;; + +let rec constraint_of_blang (blang : Blang.t) : Package_constraint.t = + match blang with + | Const b -> Package_constraint.Uop (Eq, String_literal (Bool.to_string b)) + | Not b -> Package_constraint.Not (constraint_of_blang b) + | And xs -> And (List.map ~f:constraint_of_blang xs) + | Or xs -> Or (List.map ~f:constraint_of_blang xs) + | Compare (op, lhs, rhs) -> Bop (op, var_of_sw lhs, var_of_sw rhs) + | Expr b -> Bvar (var_of_sw b) +;; + let package_fields package ~project = let open Opam_file.Create in let tags = @@ -131,7 +158,15 @@ let package_fields package ~project = | [] -> None | _ :: _ -> Some (k, list Dune_pkg.Package_dependency.opam_depend v)) in - let fields = [ optional; dep_fields ] in + let available = + match Package.enabled_if package with + | None -> [] + | Some blang -> + [ ( "available" + , constraint_of_blang blang |> Dune_pkg.Package_dependency.opam_constraint ) + ] + in + let fields = [ optional; dep_fields; available ] in let fields = let dune_version = Dune_project.dune_version project in if dune_version >= (2, 0) && tags <> [] then tags :: fields else fields @@ -176,14 +211,18 @@ let insert_dune_dep depends dune_version = let rec already_requires_odoc : Package_constraint.t -> bool = function | Uop _ | Bop _ -> true - | Bvar var -> Dune_lang.Package_variable_name.(one_of var [ with_doc; build; post ]) + | Bvar (String_literal _) -> false + | Bvar (Variable var) -> + Dune_lang.Package_variable_name.(one_of var [ with_doc; build; post ]) | And l -> List.for_all ~f:already_requires_odoc l | Or l -> List.exists ~f:already_requires_odoc l | Not t -> not (already_requires_odoc t) ;; let insert_odoc_dep depends = - let with_doc : Package_constraint.t = Bvar Dune_lang.Package_variable_name.with_doc in + let with_doc : Package_constraint.t = + Bvar (Variable Dune_lang.Package_variable_name.with_doc) + in let odoc_dep = { Package_dependency.name = odoc_name; constraint_ = Some with_doc } in let rec loop acc = function | [] -> List.rev (odoc_dep :: acc) diff --git a/src/source/only_packages.ml b/src/source/only_packages.ml index 35b6cdb7f95..07c04b15d34 100644 --- a/src/source/only_packages.ml +++ b/src/source/only_packages.ml @@ -51,30 +51,45 @@ let mem t name = ;; let mask packages ~vendored : t = - match Clflags.t () with - | No_restriction -> None - | Restrict { names; command_line_option } -> - Package.Name.Set.iter names ~f:(fun pkg_name -> - if not (Package.Name.Map.mem packages pkg_name) - then ( - let pkg_name = Package.Name.to_string pkg_name in - User_error.raise - [ Pp.textf - "I don't know about package %s (passed through %s)" - pkg_name - command_line_option - ] - ~hints: - (User_message.did_you_mean - pkg_name - ~candidates: - (Package.Name.Map.keys packages |> List.map ~f:Package.Name.to_string)))); - Package.Name.Map.filter_map packages ~f:(fun pkg -> - let name = Package.name pkg in - let vendored = Package.Name.Set.mem vendored name in - let included = Package.Name.Set.mem names name in - Option.some_if (vendored || included) pkg) - |> Option.some + let enabled, disabled = + Package.Name.Map.partition_map packages ~f:(fun (package, status) -> + match status with + | `Enabled -> Left package + | `Disabled -> Right ()) + in + match + match Clflags.t () with + | No_restriction -> None + | Restrict { names; command_line_option } -> + Package.Name.Set.iter names ~f:(fun pkg_name -> + if not (Package.Name.Map.mem packages pkg_name) + then ( + let pkg_name = Package.Name.to_string pkg_name in + User_error.raise + [ Pp.textf + "I don't know about package %s (passed through %s)" + pkg_name + command_line_option + ] + ~hints: + (User_message.did_you_mean + pkg_name + ~candidates: + (Package.Name.Map.keys packages |> List.map ~f:Package.Name.to_string)))); + Package.Name.Map.filter_map packages ~f:(fun (pkg, _) -> + let name = Package.name pkg in + let vendored = Package.Name.Set.mem vendored name in + let included = Package.Name.Set.mem names name in + Option.some_if (vendored || included) pkg) + |> Option.some + with + | None -> if Package.Name.Map.is_empty disabled then None else Some enabled + | Some p -> + Some + (Package.Name.Map.merge p enabled ~f:(fun _ masked enabled -> + match masked, enabled with + | Some x, Some _ -> Some x + | _, _ -> None)) ;; let filter_packages (t : t) packages = diff --git a/src/source/only_packages.mli b/src/source/only_packages.mli index 9d717f18814..3299c18f215 100644 --- a/src/source/only_packages.mli +++ b/src/source/only_packages.mli @@ -22,6 +22,11 @@ type t val enumerate : t -> [ `Set of Package.Name.Set.t | `All ] val mem : t -> Package.Name.t -> bool val mem_all : t -> bool -val mask : Package.t Package.Name.Map.t -> vendored:Package.Name.Set.t -> t + +val mask + : (Package.t * [ `Enabled | `Disabled ]) Package.Name.Map.t + -> vendored:Package.Name.Set.t + -> t + val filter_packages : t -> Package.t Package.Name.Map.t -> Package.t Package.Name.Map.t val filter_packages_in_project : vendored:bool -> Dune_project.t -> Dune_project.t diff --git a/test/blackbox-tests/test-cases/package-enabled_if.t b/test/blackbox-tests/test-cases/package-enabled_if.t new file mode 100644 index 00000000000..527e2127c99 --- /dev/null +++ b/test/blackbox-tests/test-cases/package-enabled_if.t @@ -0,0 +1,83 @@ +enabled_if is now allowed on packages: + + $ testProject() { + > cat > dune-project << EOF + > (lang dune 3.22) + > (using unreleased 0.1) + > (generate_opam_files true) + > (package + > (name foo) + > (dir foo) + > (enabled_if $1)) + > EOF + > out=foo/out + > dune build $out + > } + + $ mkdir foo + $ cat >foo/dune < (rule (with-stdout-to out (echo foo))) + > EOF + + $ testProject true + $ testProject false + Error: Don't know how to build foo/out + [1] + +Tests for generating opam files with the "enabled_if" field. + + $ cat > dune-project << EOF + > (lang dune 3.22) + > (using unreleased 0.1) + > (generate_opam_files true) + > (package + > (name foo) + > (enabled_if + > (and + > (<> %{os} linux) + > (or (= %{architecture} x86_64) (= %{architecture} arm64))))) + > EOF + + $ dune build @check + + $ cat foo.opam + # This file is generated by dune, edit dune-project instead + opam-version: "2.0" + depends: [ + "dune" {>= "3.22"} + "odoc" {with-doc} + ] + available: os != "linux" & (arch = "x86_64" | arch = "arm64") + build: [ + ["dune" "subst"] {dev} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] + ] + x-maintenance-intent: ["(latest)"] + +Demonstrate that we reject variables that we can't interpret: + + $ cat > dune-project << EOF + > (lang dune 3.22) + > (using unreleased 0.1) + > (generate_opam_files true) + > (package + > (name foo) + > (enabled_if (<> %{foo} bar))) + > EOF + + $ dune build @check + File "dune-project", line 6, characters 17-23: + 6 | (enabled_if (<> %{foo} bar))) + ^^^^^^ + Error: Unknown variable %{foo} + [1]