diff --git a/doc/changes/added/33.md b/doc/changes/added/33.md new file mode 100644 index 00000000000..5a25f161486 --- /dev/null +++ b/doc/changes/added/33.md @@ -0,0 +1,2 @@ +- Allow `melange.emit` runtime dependencies to be renamed with + `(source as destination)` (#33, fixes ocaml/dune#10304, @anmonteiro) diff --git a/doc/melange.rst b/doc/melange.rst index 5577f17a484..74843b780c8 100644 --- a/doc/melange.rst +++ b/doc/melange.rst @@ -230,7 +230,11 @@ In that file, ``%{melange.emit:output}`` expands to ``output/lib``. In a sources. These runtime dependencies can include assets like CSS files, images, fonts, external JavaScript files, etc. ``runtime_deps`` adhere to the formats in :doc:`concepts/dependency-spec`. For example - ``(runtime_deps ./path/to/file.css (glob_files_rec ./fonts/*))``. + ``(runtime_deps ./path/to/file.css (glob_files_rec ./fonts/*))``. Starting + with Dune 3.25, individual files can be renamed when copied by using + ``(source as destination)``. The destination is relative to the + ``melange.emit`` target directory and cannot escape it. For example, + ``(runtime_deps (index.production.html as index.html))``. - ``(emit_stdlib )`` allows the user to specify whether the Melange standard library should be included as a dependency of the stanza or not. The diff --git a/src/dune_rules/melange/melange_rules.ml b/src/dune_rules/melange/melange_rules.ml index 4261d45331d..23489e61450 100644 --- a/src/dune_rules/melange/melange_rules.ml +++ b/src/dune_rules/melange/melange_rules.ml @@ -629,12 +629,48 @@ let setup_emit_cmj_rules ;; module Runtime_deps = struct + type copy = + { src : Path.t + ; dst : Path.Build.t + ; promotion_src : Path.t + } + type targets = - { copy : (Path.t * Path.Build.t) list + { copy : copy list ; deps : Path.t list } let empty = { copy = []; deps = [] } + let make_copy src dst = { src; dst; promotion_src = src } + + let invalid_destination ~loc dst = + User_error.raise + ~loc + [ Pp.textf + "The destination path %s must be relative to the Melange target directory." + (String.maybe_quoted dst) + ] + ;; + + let destination ~loc dst = + let original = dst in + match Path.Local.of_string dst with + | dst when Path.Local.equal dst Path.Local.root -> invalid_destination ~loc original + | dst -> dst + | exception User_error.E _ -> invalid_destination ~loc original + ;; + + let expand_file_binding ~expander ~dir ~target_dir binding = + let src = File_binding.Unexpanded.src binding in + let dst_swv = File_binding.Unexpanded.dst binding |> Option.value_exn in + let dst_loc = String_with_vars.loc dst_swv in + let+ src = Expander.No_deps.expand_path expander src + and+ dst = Expander.No_deps.expand_str expander dst_swv in + let dst = destination ~loc:dst_loc dst in + let promotion_src = Path.Build.append_local dir dst |> Path.build in + let dst = Path.Build.append_local target_dir dst in + { src; dst; promotion_src } + ;; let targets = let raise_external_dep_error src ~for_ = @@ -651,43 +687,65 @@ module Runtime_deps = struct Lib_file_deps.raise_disallowed_external_path ~loc (Lib_info.name lib_info) src in fun sctx ~dir ~output ~for_ (mel : Melange_stanzas.Emit.t) -> - let+ deps = + let* deps, file_bindings = match for_ with | `Emit -> let* expander = Super_context.expander sctx ~dir in let loc, runtime_deps = mel.runtime_deps in - Lib_file_deps.eval ~expander ~loc ~paths:Allow_all runtime_deps + let runtime_deps, file_bindings = + List.partition_map runtime_deps ~f:(function + | Melange_stanzas.Runtime_deps.Dependency dependency -> Left dependency + | Melange_stanzas.Runtime_deps.File_binding binding -> Right binding) + in + let+ deps = Lib_file_deps.eval ~expander ~loc ~paths:Allow_all runtime_deps + and+ file_bindings = + match output with + | Output_kind.Private_library_or_emit target_dir -> + Memo.parallel_map file_bindings ~f:(fun binding -> + expand_file_binding ~expander ~dir ~target_dir binding) + | Public_library _ -> + Code_error.raise + "A melange.emit stanza unexpectedly has public library output" + [] + in + deps, file_bindings | `Library lib_info -> - (match Lib_info.melange_runtime_deps lib_info with - | External paths -> Memo.return (Path.Set.of_list paths) - | Local (loc, dep_conf) -> - let dir = Lib_info.src_dir (Lib_info.as_local_exn lib_info) in - let* expander = Super_context.expander sctx ~dir in - Lib_file_deps.eval ~expander ~loc ~paths:Allow_all dep_conf) - in - match output with - | Output_kind.Public_library { lib_dir; target_dir; output_dir } -> - Path.Set.fold ~init:empty deps ~f:(fun src ({ copy; deps = _ } as acc) -> - let copy = - match Path.as_external src with - | None -> - let output_dir = Path.Build.append_local target_dir output_dir in - (src, lib_output_path ~output_dir ~lib_dir src) :: copy - | Some src_e -> - (match Path.as_external lib_dir with - | Some lib_dir_e when Path.External.is_descendant src_e ~of_:lib_dir_e -> - let output_dir = Path.Build.append_local target_dir output_dir in - (src, lib_output_path ~output_dir ~lib_dir src) :: copy - | Some _ | None -> raise_external_dep_error src ~for_) + let+ deps = + match Lib_info.melange_runtime_deps lib_info with + | External paths -> Memo.return (Path.Set.of_list paths) + | Local (loc, dep_conf) -> + let dir = Lib_info.src_dir (Lib_info.as_local_exn lib_info) in + let* expander = Super_context.expander sctx ~dir in + Lib_file_deps.eval ~expander ~loc ~paths:Allow_all dep_conf in - { acc with copy }) - | Private_library_or_emit target_dir -> - Path.Set.fold ~init:empty deps ~f:(fun src ({ copy; deps } as acc) -> - match Path.as_in_build_dir src with - | None -> { acc with deps = src :: deps } - | Some src_build -> - let dst = Melange.output_path ~target_dir src_build in - { acc with copy = (src, dst) :: copy }) + deps, [] + in + let targets = + match output with + | Output_kind.Public_library { lib_dir; target_dir; output_dir } -> + Path.Set.fold ~init:empty deps ~f:(fun src ({ copy; deps = _ } as acc) -> + let copy = + match Path.as_external src with + | None -> + let output_dir = Path.Build.append_local target_dir output_dir in + make_copy src (lib_output_path ~output_dir ~lib_dir src) :: copy + | Some src_e -> + (match Path.as_external lib_dir with + | Some lib_dir_e when Path.External.is_descendant src_e ~of_:lib_dir_e -> + let output_dir = Path.Build.append_local target_dir output_dir in + make_copy src (lib_output_path ~output_dir ~lib_dir src) :: copy + | Some _ | None -> raise_external_dep_error src ~for_) + in + { acc with copy }) + | Private_library_or_emit target_dir -> + Path.Set.fold ~init:empty deps ~f:(fun src ({ copy; deps } as acc) -> + match Path.as_in_build_dir src with + | None -> { acc with deps = src :: deps } + | Some src_build -> + let dst = Melange.output_path ~target_dir src_build in + { acc with copy = make_copy src dst :: copy }) + in + Memo.return { targets with copy = List.rev_append file_bindings targets.copy } ;; end @@ -705,7 +763,7 @@ let setup_runtime_assets_rules Runtime_deps.targets sctx ~dir ~output ~for_ mel >>= fun { Runtime_deps.copy; deps } -> let loc = mel.loc in - Memo.parallel_map copy ~f:(fun (src, dst) -> + Memo.parallel_map copy ~f:(fun { Runtime_deps.src; dst; promotion_src } -> let mode = compute_promote_in_source ~promote_in_source @@ -713,7 +771,7 @@ let setup_runtime_assets_rules ~dir ~output ~mode - ~src + ~src:promotion_src ~dst in Memo.Option.bind diff --git a/src/dune_rules/melange/melange_stanzas.ml b/src/dune_rules/melange/melange_stanzas.ml index 26dfdcba4da..5ec1640a4a4 100644 --- a/src/dune_rules/melange/melange_stanzas.ml +++ b/src/dune_rules/melange/melange_stanzas.ml @@ -1,6 +1,31 @@ open Import open Dune_lang.Decoder +module Runtime_deps = struct + type t = + | Dependency of Dep_conf.t + | File_binding of File_binding.Unexpanded.t + + let decode = + peek_exn + >>= function + | List (_, [ _; Atom (_, A "as"); _ ]) -> + let+ binding = File_binding.Unexpanded.decode + and+ dune_version = Syntax.get_exn Stanza.syntax in + if dune_version < (3, 25) + then + Syntax.Error.since + (File_binding.Unexpanded.loc binding) + Stanza.syntax + (3, 25) + ~what:"Using (source as destination) in runtime_deps"; + File_binding binding + | _ -> + let+ dependency = Dep_conf.decode_no_files in + Dependency dependency + ;; +end + module Emit = struct type t = { loc : Loc.t @@ -12,7 +37,7 @@ module Emit = struct ; libraries : Lib_dep.t list ; package : Package.t option ; preprocess : Preprocess.preprocess - ; runtime_deps : Loc.t * Dep_conf.t list + ; runtime_deps : Loc.t * Runtime_deps.t list ; lint : Preprocess.Without_instrumentation.t Preprocess.Per_module.t ; promote : Rule_mode.Promote.t option ; compile_flags : Ordered_set_lang.Unexpanded.t @@ -119,10 +144,7 @@ module Emit = struct field "libraries" (Lib_dep.L.decode ~allow_re_export:false) ~default:[] and+ package = Stanza_pkg.field_opt () >>| Option.map ~f:snd and+ runtime_deps = - field - "runtime_deps" - (located (repeat Dep_conf.decode_no_files)) - ~default:(loc, []) + field "runtime_deps" (located (repeat Runtime_deps.decode)) ~default:(loc, []) and+ preprocess, preprocessor_deps = Preprocess.preprocess_fields and+ lint = field "lint" Lint.decode ~default:Lint.default and+ promote = field_o "promote" Rule_mode_decoder.Promote.decode diff --git a/src/dune_rules/melange/melange_stanzas.mli b/src/dune_rules/melange/melange_stanzas.mli index 34d25be0733..33fc8a2d95b 100644 --- a/src/dune_rules/melange/melange_stanzas.mli +++ b/src/dune_rules/melange/melange_stanzas.mli @@ -1,5 +1,11 @@ open Import +module Runtime_deps : sig + type t = + | Dependency of Dep_conf.t + | File_binding of File_binding.Unexpanded.t +end + (** Stanza to produce JavaScript targets from Melange libraries *) module Emit : sig type t = @@ -12,7 +18,7 @@ module Emit : sig ; libraries : Lib_dep.t list ; package : Package.t option ; preprocess : Preprocess.preprocess - ; runtime_deps : Loc.t * Dep_conf.t list + ; runtime_deps : Loc.t * Runtime_deps.t list ; lint : Preprocess.Without_instrumentation.t Preprocess.Per_module.t ; promote : Rule_mode.Promote.t option ; compile_flags : Ordered_set_lang.Unexpanded.t diff --git a/test/blackbox-tests/test-cases/melange/emit-with-runtime-deps.t b/test/blackbox-tests/test-cases/melange/emit-with-runtime-deps.t index 1ce9935813a..60df0c88afa 100644 --- a/test/blackbox-tests/test-cases/melange/emit-with-runtime-deps.t +++ b/test/blackbox-tests/test-cases/melange/emit-with-runtime-deps.t @@ -69,3 +69,78 @@ The runtime_dep index.txt was copied to the build folder $ node _build/default/output/main.js hello from file + +Runtime dependencies can be renamed independently in multiple emit stanzas and +promoted into their respective output directories + + $ mkdir renamed-runtime-deps + $ cd renamed-runtime-deps + $ make_melange_project 3.25 1.0 + + $ cat > dune < (melange.emit + > (alias output-a) + > (emit_stdlib false) + > (promote (until-clean) (into output_a)) + > (target output_a) + > (runtime_deps + > (index_a.html as index.html) + > (generated.html as nested/generated.html))) + > (melange.emit + > (alias output-b) + > (emit_stdlib false) + > (promote (until-clean) (into output_b)) + > (target output_b) + > (runtime_deps (index_b.html as index.html))) + > (rule + > (target generated.html) + > (action (with-stdout-to %{target} (echo generated)))) + > EOF + + $ echo index-a > index_a.html + $ echo index-b > index_b.html + + $ dune build @output-a @output-b + + $ cat output_a/index.html + index-a + $ cat output_b/index.html + index-b + $ cat output_a/nested/generated.html + generated + $ cat _build/default/output_a/index.html + index-a + $ cat _build/default/output_b/index.html + index-b + $ cat _build/default/output_a/nested/generated.html + generated + +Renamed dependencies cannot escape the emit target + + $ cat >> dune < (melange.emit + > (alias invalid-output) + > (emit_stdlib false) + > (target invalid-output) + > (runtime_deps (index_a.html as ../index.html))) + > EOF + + $ dune build @invalid-output + File "dune", line 22, characters 32-45: + 22 | (runtime_deps (index_a.html as ../index.html))) + ^^^^^^^^^^^^^ + Error: The destination path ../index.html must be relative to the Melange + target directory. + [1] + +Renaming runtime dependencies requires Dune language 3.25 + + $ make_melange_project 3.24 1.0 + $ dune build @output-a + File "dune", line 7, characters 3-15: + 7 | (index_a.html as index.html) + ^^^^^^^^^^^^ + Error: Using (source as destination) in runtime_deps is only available since + version 3.25 of the dune language. Please update your dune-project file to + have (lang dune 3.25). + [1]