diff --git a/docs/docs/development/management-commands.md b/docs/docs/development/management-commands.md index 2e5bac1f..0208cba1 100644 --- a/docs/docs/development/management-commands.md +++ b/docs/docs/development/management-commands.md @@ -46,19 +46,20 @@ Available commands: [marten] - › clearsessions Clear all expired sessions. - › collectassets Collect all the assets and copy them in a unique storage. - › gen / g Generate various structures, abstractions, and values within an existing project. - › genmigrations Generate new database migrations. - › listmigrations List all database migrations. - › migrate Run database migrations. - › new Initialize a new Marten project or application repository. - › play Start a Crystal playground server initialized for the current project. - › resetmigrations Reset an existing set of migrations into a single one. - › routes Display all the routes of the application. - › seed Populate the database by running the seed file. - › serve / s Start a development server that is automatically recompiled when source files change. - › version Show the Marten version. + › clearsessions Clear all expired sessions. + › collectartifacts Collect runtime artifacts required by compiled applications. + › collectassets Collect all the assets and copy them in a unique storage. + › gen / g Generate various structures, abstractions, and values within an existing project. + › genmigrations Generate new database migrations. + › listmigrations List all database migrations. + › migrate Run database migrations. + › new Initialize a new Marten project or application repository. + › play Start a Crystal playground server initialized for the current project. + › resetmigrations Reset an existing set of migrations into a single one. + › routes Display all the routes of the application. + › seed Populate the database by running the seed file. + › serve / s Start a development server that is automatically recompiled when source files change. + › version Show the Marten version. Run a command followed by --help to see command specific information, ex: marten [command] --help diff --git a/docs/docs/development/reference/management-commands.md b/docs/docs/development/reference/management-commands.md index 9e548c44..2b14cf4b 100644 --- a/docs/docs/development/reference/management-commands.md +++ b/docs/docs/development/reference/management-commands.md @@ -25,6 +25,35 @@ marten clearsessions # Clears all expired sessions marten clearsessions --no-input # Clears all expired sessions without any prompts ``` +## `collectartifacts` + +**Usage:** `marten collectartifacts [options]` + +Collects runtime artifacts that are required by compiled applications and copies them into a single destination +directory. + +This command collects Marten's built-in locale files, project locale files from `config/locales`, and application +locale and template files from `locales` and `templates` directories. Files are copied while preserving their paths +relative to the project compilation root, so the destination directory can be used as the [`root_path`](./settings.md#root_path) +setting in production. Symbolic links encountered in these directories are followed, and the linked files are +collected as regular files. + +Custom template loaders and directories configured through `templates.dirs` are not collected by this command. + +### Options + +* `--dest-path=PATH` - Configures where artifacts should be collected (defaults to `artifacts`) +* `--no-input` - Does not show prompts to the user +* `--app=APP_LABEL` - Collects artifacts for the specified application only + +### Examples + +```bash +marten collectartifacts # Collects all runtime artifacts into artifacts/ +marten collectartifacts --no-input --dest-path runtime_root # Collects all runtime artifacts without any prompts +marten collectartifacts --app=auth # Collects artifacts for the "auth" app only +``` + ## `collectassets` **Usage:** `marten collectassets [options]` diff --git a/spec/marten/apps/config_spec.cr b/spec/marten/apps/config_spec.cr index 583f1a9d..8c4dd9a4 100644 --- a/spec/marten/apps/config_spec.cr +++ b/spec/marten/apps/config_spec.cr @@ -87,6 +87,19 @@ describe Marten::Apps::Config do end end + describe "#artifact_dirs" do + it "returns the locales and templates directories of the application" do + app_config = Marten::Apps::ConfigSpec::AppWithTemplates::App.new + + app_config.artifact_dirs.should eq( + [ + Path[__DIR__].join("config_spec/app_with_templates/locales"), + Path[__DIR__].join("config_spec/app_with_templates/templates"), + ] + ) + end + end + describe "#assets_finder" do around_each do |t| FileUtils.rm("/tmp/marten_spec") if File.exists?("/tmp/marten_spec") diff --git a/spec/marten/cli/manage/command/collect_artifacts_spec.cr b/spec/marten/cli/manage/command/collect_artifacts_spec.cr new file mode 100644 index 00000000..c6042bc9 --- /dev/null +++ b/spec/marten/cli/manage/command/collect_artifacts_spec.cr @@ -0,0 +1,417 @@ +require "./spec_helper" +require "./collect_artifacts_spec/**" + +module Marten::CLI::Manage::Command::CollectArtifactsSpec + BROKEN_SYMLINK_PATH = "spec/config/locales/broken.yml" + CONFIG_LOCALE_PATH = "spec/config/locales/collect_artifacts_spec.yml" + DEST_PATH = "spec/collect_artifacts" + OUTSIDE_DIR = File.join(Dir.tempdir, "marten_collect_artifacts_spec_outside") + SYMLINK_TARGET_DIR = File.join(Dir.tempdir, "marten_collect_artifacts_spec_symlink_target") + SYMLINKED_DIR_PATH = "spec/config/locales/symlinked" + + def self.clean : Nil + FileUtils.rm_rf(DEST_PATH) + FileUtils.rm_rf(OUTSIDE_DIR) + FileUtils.rm_rf(SYMLINK_TARGET_DIR) + File.delete(SYMLINKED_DIR_PATH) if File.symlink?(SYMLINKED_DIR_PATH) + File.delete(BROKEN_SYMLINK_PATH) if File.symlink?(BROKEN_SYMLINK_PATH) + File.delete(CONFIG_LOCALE_PATH) if File.exists?(CONFIG_LOCALE_PATH) + Dir.delete("spec/config/locales") rescue nil + Dir.delete("spec/config") rescue nil + end + + def self.destination_path(path) + File.join(DEST_PATH, path) + end + + def self.prepare_project_locale : Nil + FileUtils.mkdir_p(Path[CONFIG_LOCALE_PATH].dirname) + File.write(CONFIG_LOCALE_PATH, <<-YAML) + en: + collect_artifacts_spec_project: Project + YAML + end +end + +describe Marten::CLI::Manage::Command::CollectArtifacts do + with_installed_apps( + Marten::CLI::Manage::Command::CollectArtifactsSpec::AppWithArtifacts, + Marten::CLI::Manage::Command::CollectArtifactsSpec::EmptyApp + ) + + with_main_app_location "#{__DIR__}/../../../../test_project" + + describe "#run" do + before_each do + Marten::CLI::Manage::Command::CollectArtifactsSpec.clean + Marten::CLI::Manage::Command::CollectArtifactsSpec.prepare_project_locale + end + + after_each do + Marten::CLI::Manage::Command::CollectArtifactsSpec.clean + end + + it "warns the user that files might be overwritten by default" do + stdin = IO::Memory.new("n") + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: [] of String, + stdin: stdin, + stdout: stdout + ) + + command.handle + + stdout.rewind.gets_to_end.starts_with?( + "Runtime artifacts will be collected into 'artifacts'.\n" \ + "Any existing files will be overwritten.\n" \ + "Do you want to continue [yes/no]?" + ).should be_true + end + + it "does not do anything if the user inputs that they do not want to proceed" do + stdin = IO::Memory.new("no") + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: ["--dest-path", Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH], + stdin: stdin, + stdout: stdout + ) + + command.handle + + stdout.rewind.gets_to_end.should eq( + "Runtime artifacts will be collected into " \ + "'#{Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH}'.\n" \ + "Any existing files will be overwritten.\n" \ + "Do you want to continue [yes/no]? " \ + "Cancelling...\n" + ) + + Dir.exists?(Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH).should be_false + end + + it "copies the runtime artifacts as expected if the user inputs that they want to proceed" do + stdin = IO::Memory.new("yes") + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: ["--no-color", "--dest-path", Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH], + stdin: stdin, + stdout: stdout + ) + + command.handle + + output = stdout.rewind.gets_to_end + output.includes?("Collecting runtime artifacts:\n").should be_true + output.includes?("Copying src/marten/locales/en.yml...").should be_true + output.includes?("Copying config/locales/collect_artifacts_spec.yml...").should be_true + output.includes?("Copying spec/test_project/templates/base.html...").should be_true + output.includes?( + "Copying spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/example.html..." + ).should be_true + + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path("src/marten/locales/en.yml") + ).should be_true + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path( + "config/locales/collect_artifacts_spec.yml" + ) + ).should be_true + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path("spec/test_project/locales/en.yml") + ).should be_true + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path("spec/test_project/templates/base.html") + ).should be_true + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path( + "spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/locales/en.yml" + ) + ).should be_true + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path( + "spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/example.html" + ) + ).should be_true + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path( + "spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/partials/nested.html" + ) + ).should be_true + end + + it "accepts 'y' as a confirmation that the user wants to proceed" do + stdin = IO::Memory.new("y") + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: ["--dest-path", Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH], + stdin: stdin, + stdout: stdout + ) + + command.handle + + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path("src/marten/locales/en.yml") + ).should be_true + end + + it "accepts badly inputted 'yes' values as a confirmation that the user wants to proceed" do + stdin = IO::Memory.new("YeS") + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: ["--dest-path", Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH], + stdin: stdin, + stdout: stdout + ) + + command.handle + + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path("src/marten/locales/en.yml") + ).should be_true + end + + it "does not prompt the user before collecting artifacts if the --no-input option is used" do + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: [ + "--no-input", + "--dest-path", + Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH, + ], + stdout: stdout + ) + + command.handle + + output = stdout.rewind.gets_to_end + output.includes?("Do you want to continue").should be_false + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path("src/marten/locales/en.yml") + ).should be_true + end + + it "overwrites existing files in the destination directory" do + destination_path = Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path( + "spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/example.html" + ) + FileUtils.mkdir_p(Path[destination_path].dirname) + File.write(destination_path, "Old template") + + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: [ + "--no-input", + "--app", + "collect_artifacts_spec_app", + "--dest-path", + Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH, + ], + stdout: stdout + ) + + command.handle + + File.read(destination_path).should eq "Example template\n" + end + + it "copies hidden files" do + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: [ + "--no-input", + "--app", + "collect_artifacts_spec_app", + "--dest-path", + Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH, + ], + stdout: stdout + ) + + command.handle + + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path( + "spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/locales/.hidden.yml" + ) + ).should be_true + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path( + "spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/.hidden.html" + ) + ).should be_true + end + + it "collects artifacts behind symlinked directories" do + FileUtils.mkdir_p(Marten::CLI::Manage::Command::CollectArtifactsSpec::SYMLINK_TARGET_DIR) + File.write( + File.join(Marten::CLI::Manage::Command::CollectArtifactsSpec::SYMLINK_TARGET_DIR, "en.yml"), + "en:\n collect_artifacts_spec_symlinked: Symlinked\n" + ) + File.symlink( + Marten::CLI::Manage::Command::CollectArtifactsSpec::SYMLINK_TARGET_DIR, + Marten::CLI::Manage::Command::CollectArtifactsSpec::SYMLINKED_DIR_PATH + ) + + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: [ + "--no-input", + "--dest-path", + Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH, + ], + stdout: stdout + ) + + command.handle + + File.read( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path("config/locales/symlinked/en.yml") + ).should eq "en:\n collect_artifacts_spec_symlinked: Symlinked\n" + end + + it "exits with a non-zero status if an artifact cannot be copied" do + File.symlink( + File.join(Marten::CLI::Manage::Command::CollectArtifactsSpec::SYMLINK_TARGET_DIR, "missing.yml"), + Marten::CLI::Manage::Command::CollectArtifactsSpec::BROKEN_SYMLINK_PATH + ) + + stdout = IO::Memory.new + stderr = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: [ + "--no-input", + "--dest-path", + Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH, + ], + stdout: stdout, + stderr: stderr, + exit_raises: true + ) + + command.handle.should eq 1 + + stderr.rewind.gets_to_end.includes?("Could not copy").should be_true + end + + it "collects artifacts for a specific application only if the --app option is used" do + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: [ + "--no-input", + "--app", + "collect_artifacts_spec_app", + "--dest-path", + Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH, + ], + stdout: stdout + ) + + command.handle + + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path( + "spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/locales/en.yml" + ) + ).should be_true + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path( + "spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/partials/nested.html" + ) + ).should be_true + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path("src/marten/locales/en.yml") + ).should be_false + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path( + "config/locales/collect_artifacts_spec.yml" + ) + ).should be_false + File.exists?( + Marten::CLI::Manage::Command::CollectArtifactsSpec.destination_path("spec/test_project/templates/base.html") + ).should be_false + end + + it "exits with a non-zero status if the specified app label is not associated with any existing app" do + stdout = IO::Memory.new + stderr = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: ["--no-input", "--app", "unknown_app"], + stdout: stdout, + stderr: stderr, + exit_raises: true + ) + + command.handle.should eq 1 + + stderr.rewind.gets_to_end.includes?( + "Label 'unknown_app' is not associated with any installed apps" + ).should be_true + end + + it "exits with a non-zero status if an artifact lives outside the compilation root" do + outside_dir = Marten::CLI::Manage::Command::CollectArtifactsSpec::OUTSIDE_DIR + FileUtils.mkdir_p(File.join(outside_dir, "locales")) + File.write(File.join(outside_dir, "locales", "en.yml"), <<-YAML) + en: + collect_artifacts_spec_outside: Outside + YAML + Marten::Apps::MainConfig.overridden_location = outside_dir + + stdout = IO::Memory.new + stderr = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: [ + "--no-input", + "--app", + "main", + "--dest-path", + Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH, + ], + stdout: stdout, + stderr: stderr, + exit_raises: true + ) + + command.handle.should eq 1 + + stderr.rewind.gets_to_end.includes?("outside the compilation root").should be_true + end + + it "prints the expected message if no artifacts are found" do + stdout = IO::Memory.new + + command = Marten::CLI::Manage::Command::CollectArtifacts.new( + options: [ + "--no-color", + "--no-input", + "--app", + "collect_artifacts_spec_empty_app", + "--dest-path", + Marten::CLI::Manage::Command::CollectArtifactsSpec::DEST_PATH, + ], + stdout: stdout + ) + + command.handle + + stdout.rewind.gets_to_end.includes?("No artifacts to collect...").should be_true + end + end +end diff --git a/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/app.cr b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/app.cr new file mode 100644 index 00000000..45acc1f7 --- /dev/null +++ b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/app.cr @@ -0,0 +1,5 @@ +module Marten::CLI::Manage::Command::CollectArtifactsSpec + class AppWithArtifacts < Marten::App + label :collect_artifacts_spec_app + end +end diff --git a/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/locales/.hidden.yml b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/locales/.hidden.yml new file mode 100644 index 00000000..7166c569 --- /dev/null +++ b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/locales/.hidden.yml @@ -0,0 +1,2 @@ +en: + collect_artifacts_spec_app_hidden: Hidden diff --git a/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/locales/en.yml b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/locales/en.yml new file mode 100644 index 00000000..472b211e --- /dev/null +++ b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/locales/en.yml @@ -0,0 +1,2 @@ +en: + collect_artifacts_spec_app: App diff --git a/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/.hidden.html b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/.hidden.html new file mode 100644 index 00000000..33c4b915 --- /dev/null +++ b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/.hidden.html @@ -0,0 +1 @@ +Hidden template diff --git a/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/example.html b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/example.html new file mode 100644 index 00000000..b5d24716 --- /dev/null +++ b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/example.html @@ -0,0 +1 @@ +Example template diff --git a/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/partials/nested.html b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/partials/nested.html new file mode 100644 index 00000000..42193575 --- /dev/null +++ b/spec/marten/cli/manage/command/collect_artifacts_spec/app_with_artifacts/templates/partials/nested.html @@ -0,0 +1 @@ +Nested template diff --git a/spec/marten/cli/manage/command/collect_artifacts_spec/empty_app/app.cr b/spec/marten/cli/manage/command/collect_artifacts_spec/empty_app/app.cr new file mode 100644 index 00000000..cf81d750 --- /dev/null +++ b/spec/marten/cli/manage/command/collect_artifacts_spec/empty_app/app.cr @@ -0,0 +1,5 @@ +module Marten::CLI::Manage::Command::CollectArtifactsSpec + class EmptyApp < Marten::App + label :collect_artifacts_spec_empty_app + end +end diff --git a/src/marten/apps/config.cr b/src/marten/apps/config.cr index 0d867537..30bab7e0 100644 --- a/src/marten/apps/config.cr +++ b/src/marten/apps/config.cr @@ -56,6 +56,16 @@ module Marten super || (label == other.label && models == other.models) end + # Returns the build-time directories containing the runtime artifacts of the application. + # + # These correspond to the application's `locales` and `templates` directories, whose files must be shipped + # alongside compiled applications. The returned directories are not guaranteed to exist. Applications with + # custom layouts can override this method. + def artifact_dirs : Array(Path) + app_location = Path[self.class._marten_app_location] + [app_location.join(LOCALES_DIR), app_location.join(TEMPLATES_DIR)] + end + # Returns the assets finder of the application. # # If the application doesn't have a dedicated assets directory, `nil` is returned. diff --git a/src/marten/cli/manage/command/collect_artifacts.cr b/src/marten/cli/manage/command/collect_artifacts.cr new file mode 100644 index 00000000..858eb6c1 --- /dev/null +++ b/src/marten/cli/manage/command/collect_artifacts.cr @@ -0,0 +1,163 @@ +module Marten + module CLI + class Manage + module Command + class CollectArtifacts < Base + command_name :collectartifacts + help "Collect runtime artifacts required by compiled applications." + + @app_label : String? + @dest_path : String = "artifacts" + @no_input : Bool = false + + def setup + on_option_with_arg( + "dest-path", + arg: "Path", + description: %(Specify where artifacts should be collected (defaults to "artifacts").) + ) do |v| + @dest_path = v + end + + on_option("no-input", "Do not show prompts to the user") { @no_input = true } + + on_option_with_arg( + "app", + arg: "Label", + description: "Collect artifacts for a specific application only." + ) do |v| + @app_label = v + end + end + + def run + artifacts = discover_artifacts + + unless no_input? + print("Runtime artifacts will be collected into '#{dest_path}'.") + print("Any existing files will be overwritten.") + print("Do you want to continue [yes/no]?", ending: " ") + unless %w(y yes).includes?(stdin.gets.to_s.downcase) + print("Cancelling...") + return + end + end + + print(style("Collecting runtime artifacts:", fore: :light_blue, mode: :bold), ending: "\n") + collect(artifacts) + end + + private record Artifact, source_path : Path, relative_path : Path + private record Source, dir : Path, dest_prefix : Path + + private getter app_label + private getter dest_path + + private def add_app_sources(sources, app_config) + app_config.artifact_dirs.each do |dir| + sources << build_source(dir) if Dir.exists?(dir) + end + end + + private def app_config(label) + Marten.apps.get(label) + rescue e : Apps::Errors::AppNotFound + print_error_and_exit(e.message) + end + + private def artifact_sources + sources = [] of Source + + if label = app_label + add_app_sources(sources, app_config(label)) + else + # Mirrors the built-in locales lookup performed by Marten.setup_i18n. + marten_locales_dir = Path[Marten._marten_app_location].join("marten", "locales") + sources << build_source(marten_locales_dir) if Dir.exists?(marten_locales_dir) + + # Mirrors the Marten.root.join("config/locales") lookup performed by Marten.setup_i18n: project + # locales are anchored at the destination root so that the collected tree matches the runtime + # lookup when the root_path setting points to the destination directory. + project_locales_dir = Path[Marten.apps.main.class._marten_app_location] + .join("..", "config", "locales") + .expand + if Dir.exists?(project_locales_dir) + sources << Source.new(project_locales_dir, Path["config"].join("locales")) + end + + Marten.apps.app_configs.each do |app_config| + add_app_sources(sources, app_config) + end + end + + sources.uniq(&.dir.expand) + end + + private def build_source(dir) + relative_dir = dir.relative_to(compilation_root_path) + + if relative_dir.to_s == ".." || relative_dir.to_s.starts_with?("../") + print_error_and_exit( + "Cannot collect artifacts from '#{dir}' because it is outside the compilation root " \ + "'#{compilation_root_path}'" + ) + end + + Source.new(dir, relative_dir) + end + + private def collect(artifacts) + collected_count = 0 + + artifacts.each do |artifact| + copy_artifact_file(artifact) + collected_count += 1 + end + + if collected_count == 0 + print("No artifacts to collect...") + end + end + + private def compilation_root_path + Path[Marten::Apps::Config.compilation_root_path] + end + + private def copy_artifact_file(artifact) + destination_path = Path[dest_path].join(artifact.relative_path) + + print(" › Copying #{style(artifact.relative_path.to_s, mode: :dim)}...", ending: "") + + begin + FileUtils.mkdir_p(destination_path.dirname) + FileUtils.cp(artifact.source_path, destination_path) + rescue e : IO::Error + print(style(" ERROR", fore: :red, mode: :bold)) + print_error_and_exit("Could not copy '#{artifact.source_path}': #{e.message}") + end + + print(style(" DONE", fore: :light_green, mode: :bold)) + end + + private def discover_artifacts + artifacts = artifact_sources.flat_map do |source| + Dir.glob( + source.dir.join("**", "*").to_s, + match: File::MatchOptions.glob_default | File::MatchOptions::DotFiles, + follow_symlinks: true + ) + .reject { |path| File.directory?(path) } + .map { |path| Artifact.new(Path[path], source.dest_prefix.join(Path[path].relative_to(source.dir))) } + end + + artifacts.sort_by!(&.relative_path.to_s) + end + + private def no_input? + @no_input + end + end + end + end + end +end