Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/changes/added/33.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Allow `melange.emit` runtime dependencies to be renamed with
`(source as destination)` (#33, fixes ocaml/dune#10304, @anmonteiro)
6 changes: 5 additions & 1 deletion doc/melange.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <bool>)`` allows the user to specify whether the Melange
standard library should be included as a dependency of the stanza or not. The
Expand Down
126 changes: 92 additions & 34 deletions src/dune_rules/melange/melange_rules.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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_ =
Expand All @@ -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

Expand All @@ -705,15 +763,15 @@ 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
~project:(Scope.project scope)
~dir
~output
~mode
~src
~src:promotion_src
~dst
in
Memo.Option.bind
Expand Down
32 changes: 27 additions & 5 deletions src/dune_rules/melange/melange_stanzas.ml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion src/dune_rules/melange/melange_stanzas.mli
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -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
Expand Down
75 changes: 75 additions & 0 deletions test/blackbox-tests/test-cases/melange/emit-with-runtime-deps.t
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
> (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 <<EOF
> (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]
Loading