From 5c4893735cacf0630a4c2e942a0f828d4d30a3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Thu, 20 Mar 2025 15:43:09 +0100 Subject: [PATCH 1/3] Windows support. --- .devspace.toml | 2 +- .editorconfig | 7 +++ .github/workflows/build.yml | 16 +++++++ .github/workflows/ci-base.yml | 4 +- .tool-versions | 2 +- Dockerfile | 2 +- Makefile | 2 +- docker-compose.yml | 2 +- spec/compilers_spec.cr | 5 ++- spec/ext/uri_spec.cr | 16 +++++++ spec/language_server_spec.cr | 5 ++- spec/mint_json_spec.cr | 10 ++--- spec/static_documentation_generator_spec.cr | 3 ++ spec/watcher_spec.cr | 4 +- spec_cli/build_spec.cr | 11 +++-- spec_cli/spec_helper.cr | 26 +++++++++++ src/ast/directives/file_based.cr | 4 ++ src/bundler.cr | 5 ++- src/compilers/dbg.cr | 2 +- src/compilers/suite.cr | 2 +- src/compilers/test.cr | 2 +- src/ext/file.cr | 2 +- src/ext/string.cr | 8 ++++ src/ext/uri.cr | 15 +++++++ src/ls/code_actions/module_actions.cr | 2 +- src/ls/code_actions/provider_actions.cr | 2 +- src/ls/definitions.cr | 2 +- src/ls/diagnostics_provider.cr | 4 +- src/ls/document_symbol.cr | 2 +- src/ls/hover.cr | 2 +- src/ls/sandbox.cr | 2 +- src/ls/semantic_tokens.cr | 2 +- src/lsp/protocol/text_document_identifier.cr | 2 +- src/lsp/protocol/text_document_item.cr | 2 +- src/message.cr | 6 +-- src/parser/file.cr | 4 ++ src/test_runner/browser.cr | 10 ++++- src/type_checkers/directives/asset.cr | 2 +- .../directives/highlight_file.cr | 2 +- src/type_checkers/directives/inline.cr | 2 +- src/type_checkers/directives/svg.cr | 2 +- src/utils/icon_generator.cr | 43 ++++++++++++++++++- src/utils/source_files.cr | 4 +- 43 files changed, 200 insertions(+), 54 deletions(-) create mode 100644 .editorconfig create mode 100644 spec/ext/uri_spec.cr create mode 100644 src/ext/uri.cr diff --git a/.devspace.toml b/.devspace.toml index 1ea6269a4..ad8dc078e 100644 --- a/.devspace.toml +++ b/.devspace.toml @@ -1,7 +1,7 @@ # This is for doing development in a container using devspace # (https://github.com/gdotdesign/devspace). -image = "crystallang/crystal:1.19.1-alpine" +image = "crystallang/crystal:1.21.0-alpine" name = "mint" gui = true diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..16bf530a1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 299275da5..96bf0cdc8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,6 +33,9 @@ jobs: - os: macos-latest openssl: 3 + - os: windows-latest + openssl: default + runs-on: ${{ matrix.os }} steps: @@ -49,6 +52,7 @@ jobs: uses: rlespinasse/github-slug-action@v5 - name: Set architecture environment variable + shell: bash run: | if [ "$RUNNER_ARCH" == "X64" ]; then echo "ARCH=x86_64" >> $GITHUB_ENV @@ -59,6 +63,7 @@ jobs: fi - name: Set OpenSSL suffix + shell: bash run: | if [ "${{ matrix.openssl }}" == "3" ]; then echo "OPENSSL_SUFFIX=-openssl3" >> $GITHUB_ENV @@ -128,6 +133,17 @@ jobs: -o build/mint-${GITHUB_REF_SLUG}-osx-${ARCH}${OPENSSL_SUFFIX} \ --no-debug --release + # We are linking statically so the binary can be used on its own, + # otherwise the DLLs it's linked against need to be next to it. + - if: matrix.os == 'windows-latest' + name: Build binary (Windows) + shell: bash + run: | + mkdir build + crystal build src/mint.cr \ + -o build/mint-${GITHUB_REF_SLUG}-windows-${ARCH}.exe \ + --static --no-debug --release + - name: Upload artifacts uses: actions/upload-artifact@v7 with: diff --git a/.github/workflows/ci-base.yml b/.github/workflows/ci-base.yml index c767af7b8..d3c20331c 100644 --- a/.github/workflows/ci-base.yml +++ b/.github/workflows/ci-base.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, macos-15-intel] + os: [ubuntu-latest, macos-latest, macos-15-intel, windows-latest] runs-on: ${{ matrix.os }} timeout-minutes: 15 @@ -45,7 +45,7 @@ jobs: run: shards build mint --error-on-warnings --error-trace - name: Run CLI specs - run: crystal spec spec_cli/*_spec.cr spec_cli/**/*_spec.cr --error-on-warnings --error-trace + run: crystal spec spec_cli --error-on-warnings --error-trace - name: Run core specs (Firefox) working-directory: ./core/tests diff --git a/.tool-versions b/.tool-versions index 98695993f..06c6fb886 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ -crystal 1.19.1 +crystal 1.21.0 mint 0.20.0 nodejs 20.10.0 yarn 1.22.19 diff --git a/Dockerfile b/Dockerfile index 9f0d4a37a..e1075e5a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # We are using crystal as image we are building the binary on -FROM crystallang/crystal:1.19.1-alpine AS build +FROM crystallang/crystal:1.21.0-alpine AS build # Create a build directory and set it as default RUN mkdir -p /opt/mint diff --git a/Makefile b/Makefile index cfe33d9eb..d1f5ff3ff 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ spec: .PHONY: spec-cli spec-cli: build - crystal spec spec_cli/*_spec.cr spec_cli/**/*_spec.cr --error-on-warnings --error-trace --progress + crystal spec spec_cli --error-on-warnings --error-trace --progress .PHONY: format format: diff --git a/docker-compose.yml b/docker-compose.yml index 5f0aa3c0f..0460c59a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ version: "2" services: app: - image: crystallang/crystal:1.19.1-alpine + image: crystallang/crystal:1.21.0-alpine working_dir: /app volumes: - .:/app diff --git a/spec/compilers_spec.cr b/spec/compilers_spec.cr index 2f7845f45..b2f38f55e 100644 --- a/spec/compilers_spec.cr +++ b/spec/compilers_spec.cr @@ -30,8 +30,11 @@ Dir {Config.new, raw} end + file_path = + Path[File.dirname(__FILE__), "../", file].normalize.to_s + # Parse the sample - ast = Mint::Parser.parse(sample, File.dirname(__FILE__) + file.lchop("./spec")) + ast = Mint::Parser.parse(sample, file_path) ast.class.should eq(Mint::Ast) artifacts = diff --git a/spec/ext/uri_spec.cr b/spec/ext/uri_spec.cr new file mode 100644 index 000000000..ecaf3e608 --- /dev/null +++ b/spec/ext/uri_spec.cr @@ -0,0 +1,16 @@ +require "../spec_helper" + +describe "URI.file_path" do + it "returns unix file path" do + URI.parse("file://etc/hosts").file_path.should eq("/hosts") + end + + it "return unix file path" do + URI.parse("file:///etc/hosts").file_path.should eq("/etc/hosts") + end + + it "return windows file path" do + URI.parse("file:///c:/project/readme.md").file_path.should eq("C:\\project\\readme.md") + URI.parse("file:///C%3A/project/readme.md").file_path.should eq("C:\\project\\readme.md") + end +end diff --git a/spec/language_server_spec.cr b/spec/language_server_spec.cr index 1735fe3b4..578f764e3 100644 --- a/spec/language_server_spec.cr +++ b/spec/language_server_spec.cr @@ -1,7 +1,7 @@ require "./spec_helper" def clean_json(workspace : Workspace, path : String) - path.strip.gsub("\#{root_path}", workspace.root_path) + path.strip.gsub("\#{root_path}", workspace.root_path.to_lsp_path) end Dir @@ -73,7 +73,8 @@ Dir json.to_json.should contain(expected.to_json) else begin - expected_response[0].should eq(actual_response) + JSON.parse(actual_response) + .should eq(JSON.parse(expected_response[0])) rescue fail diff(expected_response[0], actual_response) end diff --git a/spec/mint_json_spec.cr b/spec/mint_json_spec.cr index c08eaa9a2..c1a817de9 100644 --- a/spec/mint_json_spec.cr +++ b/spec/mint_json_spec.cr @@ -21,15 +21,11 @@ Dir end it "non existent file" do + # The OS specific error message differs so we only check the beginning. Mint::MintJson.parse("test.json") rescue error : Mint::Error - error.to_terminal.to_s.uncolorize.should eq(<<-TEXT) - ░ ERROR (MINT_JSON_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ - - There was a problem trying to open a mint.json file: test.json - - Error opening file with mode 'r': 'test.json': No such file or directory - TEXT + error.to_terminal.to_s.uncolorize.should contain( + "There was a problem trying to open a mint.json file: test.json") end it "no mint.json in directory or parents" do diff --git a/spec/static_documentation_generator_spec.cr b/spec/static_documentation_generator_spec.cr index b3644df66..29ffcfd27 100644 --- a/spec/static_documentation_generator_spec.cr +++ b/spec/static_documentation_generator_spec.cr @@ -1,3 +1,6 @@ +# We don't test this on windows because there is no `xmllint`. +{% skip_file if flag?(:windows) %} + require "./spec_helper" Dir diff --git a/spec/watcher_spec.cr b/spec/watcher_spec.cr index 394960e39..665c6194f 100644 --- a/spec/watcher_spec.cr +++ b/spec/watcher_spec.cr @@ -21,7 +21,7 @@ describe Mint::Watcher do watcher = Mint::Watcher .new { |items| modified = items } - .tap(&.patterns = ["#{directory}/**/*"]) + .tap(&.patterns = ["#{directory.to_posix}/**/*"]) # Returns all files modified.should eq([file1, file2]) @@ -37,7 +37,7 @@ describe Mint::Watcher do modified.should eq([file2]) # Returns all files - watcher.patterns = ["#{directory}/**/*"] + watcher.patterns = ["#{directory.to_posix}/**/*"] watcher.scan(:modified) modified.should eq([file1]) ensure diff --git a/spec_cli/build_spec.cr b/spec_cli/build_spec.cr index fa85dff2b..0b5d7004c 100644 --- a/spec_cli/build_spec.cr +++ b/spec_cli/build_spec.cr @@ -13,7 +13,7 @@ context "build" do end it "displays help with '--help' flag" do - expect_output ["build", "--help"], <<-TEXT + expect_output %w[build --help], <<-TEXT Usage: ×××× build [flags...] [arg...] @@ -36,7 +36,7 @@ context "build" do end it "builds the project" do - expect_output ["build"], <<-TEXT + expect_output %w[build --skip-icons], <<-TEXT Mint - Building for production ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⚙ Clearing the "dist" directory... ×××× @@ -50,7 +50,7 @@ context "build" do end it "shows timings with the `--timings` flag" do - expect_output ["build", "--timings"], <<-TEXT + expect_output %w[build --timings --skip-icons], <<-TEXT Mint - Building for production ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⚙ Clearing the "dist" directory... ×××× @@ -66,7 +66,6 @@ context "build" do Calculating dependencies for bundles... | ×××× Bundling and generating JavaScript... | ×××× Generating index.html | ×××× - Generating icons | ×××× Copying assets | ×××× Generating index.css | ×××× ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -75,7 +74,7 @@ context "build" do end it "generates manifeset with the `--generate-manifest` flag" do - expect_output ["build", "--generate-manifest"], <<-TEXT + expect_output %w[build --generate-manifest --skip-icons], <<-TEXT Mint - Building for production ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⚙ Clearing the "dist" directory... ×××× @@ -91,7 +90,7 @@ context "build" do end it "logs the files using the `--verbose` flag" do - expect_output ["build", "--verbose"], <<-TEXT + expect_output %w[build --verbose --skip-icons], <<-TEXT Mint - Building for production ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⚙ Clearing the "dist" directory... ×××× diff --git a/spec_cli/spec_helper.cr b/spec_cli/spec_helper.cr index 6e4c84344..23d4ca3f5 100644 --- a/spec_cli/spec_helper.cr +++ b/spec_cli/spec_helper.cr @@ -14,6 +14,12 @@ def run(args : Array(String), input : String = "", clear_env = true) path = Path[__DIR__, "..", "bin", "mint"].normalize.to_s + # On Windows the binary needs `PATH` to find the DLLs it is linked against, + # without them it exits before `main` runs (no output, no exit code). + {% if flag?(:windows) %} + clear_env = false + {% end %} + status = Process.run( clear_env: clear_env, @@ -26,6 +32,26 @@ def run(args : Array(String), input : String = "", clear_env = true) "NO_COLOR" => "1", }) + # A command is allowed to fail (that's what some of the specs check) but it + # must not crash, otherwise the specs fail in confusing ways later on. + unless status.normal_exit? + raise <<-TEXT + COMMAND CRASHED! + + COMMAND: + #{path} #{args.join(' ')} + + EXIT: + #{status.exit_reason} + + OUTPUT: + #{output.rewind.gets_to_end} + + ERROR: + #{error.rewind.gets_to_end} + TEXT + end + { output.rewind.gets_to_end, error.rewind.gets_to_end, diff --git a/src/ast/directives/file_based.cr b/src/ast/directives/file_based.cr index dfbceaf28..e0168a8e7 100644 --- a/src/ast/directives/file_based.cr +++ b/src/ast/directives/file_based.cr @@ -14,6 +14,10 @@ module Mint File.relative_path_from_ancestor(path, "mint.json") end + getter relative_path_posix : String do + Path[relative_path].to_posix.to_s + end + # The real path of the asset on the disk. getter real_path : Path do if path.starts_with?("/") diff --git a/src/bundler.cr b/src/bundler.cr index 0e473f0d5..0f3d5d944 100644 --- a/src/bundler.cr +++ b/src/bundler.cr @@ -441,8 +441,9 @@ module Mint def generate_icons? !config.skip_icons && - Process.find_executable("convert") && - File.exists?(json.application.icon) + !json.application.icon.blank? && + !IconGenerator.executable.nil? && + File.exists?(File.join(File.dirname(json.path), json.application.icon)) end def generate_icons diff --git a/src/compilers/dbg.cr b/src/compilers/dbg.cr index 02bc40c2e..535a32896 100644 --- a/src/compilers/dbg.cr +++ b/src/compilers/dbg.cr @@ -10,7 +10,7 @@ module Mint end else location = - js.string("#{node.file.relative_path}:#{node.from.line}:#{node.from.column}") + js.string("#{node.file.relative_path_posix}:#{node.from.line}:#{node.from.column}") var = [Variable.new] of Item diff --git a/src/compilers/suite.cr b/src/compilers/suite.cr index 7cb0c7ede..d0447b657 100644 --- a/src/compilers/suite.cr +++ b/src/compilers/suite.cr @@ -7,7 +7,7 @@ module Mint Raw.new({ start: {node.from.line, node.from.column}, end: {node.to.line, node.to.column}, - filename: node.file.relative_path, + filename: node.file.relative_path_posix, }.to_json), ] diff --git a/src/compilers/test.cr b/src/compilers/test.cr index d6960dd9e..9b3b3f064 100644 --- a/src/compilers/test.cr +++ b/src/compilers/test.cr @@ -7,7 +7,7 @@ module Mint Raw.new({ start: {node.from.line, node.from.column}, end: {node.to.line, node.to.column}, - filename: node.file.relative_path, + filename: node.file.relative_path_posix, }.to_json), ] diff --git a/src/ext/file.cr b/src/ext/file.cr index 0d6b38a5a..f3e79fb7d 100644 --- a/src/ext/file.cr +++ b/src/ext/file.cr @@ -13,7 +13,7 @@ class File root = File.dirname(base) loop do - return if root == "." || root == "/" + return if root.in?(".", "/", Path[root].anchor.to_s) # ameba:disable Lint/AssignmentInCallArgument if File.exists?(path = Path[root, name]) diff --git a/src/ext/string.cr b/src/ext/string.cr index fff073f80..8dfb4baaa 100644 --- a/src/ext/string.cr +++ b/src/ext/string.cr @@ -19,6 +19,14 @@ class String lines.join('\n', &.rstrip) end + def to_lsp_path + if matches_full?(/([A-Z]):\\(.*)/i) + "/#{gsub('\\', '/')}" + else + self + end + end + def shrink_to_minimum_leading_whitespace : String # We start from the maximum number for indent size indent_size = diff --git a/src/ext/uri.cr b/src/ext/uri.cr new file mode 100644 index 000000000..81b276120 --- /dev/null +++ b/src/ext/uri.cr @@ -0,0 +1,15 @@ +class URI + # Returns proper file path on Windows and Unix + def file_path : String + raise "Not a file path!" unless scheme == "file" + + path = + URI.decode(self.path.lchop("/")) + + if match = /([A-Z]):\/(.*)/i.match(path) + "#{match[1].upcase}:\\#{match[2].gsub('/', '\\')}" + else + self.path + end + end +end diff --git a/src/ls/code_actions/module_actions.cr b/src/ls/code_actions/module_actions.cr index 94462e852..600da2de8 100644 --- a/src/ls/code_actions/module_actions.cr +++ b/src/ls/code_actions/module_actions.cr @@ -16,7 +16,7 @@ module Mint node.functions.sort_by(&.name.value)) .each_with_index { |entity, index| entity.from = order[index] } - case formatted = workspace.format(URI.parse(uri).path.to_s) + case formatted = workspace.format(URI.parse(uri).file_path) in String LSP::CodeAction.new( title: "Order Entities", diff --git a/src/ls/code_actions/provider_actions.cr b/src/ls/code_actions/provider_actions.cr index eb60d14c5..6254a5304 100644 --- a/src/ls/code_actions/provider_actions.cr +++ b/src/ls/code_actions/provider_actions.cr @@ -18,7 +18,7 @@ module Mint node.functions.sort_by(&.name.value)) .each_with_index { |entity, index| entity.from = order[index] } - case formatted = workspace.format(URI.parse(uri).path.to_s) + case formatted = workspace.format(URI.parse(uri).file_path) in String LSP::CodeAction.new( title: "Order Entities", diff --git a/src/ls/definitions.cr b/src/ls/definitions.cr index e0b0d5569..5502043a5 100644 --- a/src/ls/definitions.cr +++ b/src/ls/definitions.cr @@ -54,7 +54,7 @@ module Mint LSP::LocationLink.new( origin_selection_range: to_lsp_range(source), target_selection_range: to_lsp_range(target), - target_uri: "file://#{target.file.path}", + target_uri: "file://#{target.file.path.to_lsp_path}", target_range: to_lsp_range(parent), ) end diff --git a/src/ls/diagnostics_provider.cr b/src/ls/diagnostics_provider.cr index f75c0e99b..1070d2b1d 100644 --- a/src/ls/diagnostics_provider.cr +++ b/src/ls/diagnostics_provider.cr @@ -10,7 +10,7 @@ module Mint def process(result : Workspace::Result) @current.each do |path| @server.send_notification("textDocument/publishDiagnostics", { - uri: "file://#{path}", + uri: "file://#{path.to_lsp_path}", diagnostics: [] of String, }) end @@ -26,7 +26,7 @@ module Mint diagnostics.each do |path, items| @server.send_notification("textDocument/publishDiagnostics", { - uri: "file://#{path}", + uri: "file://#{path.to_lsp_path}", diagnostics: items, }) end diff --git a/src/ls/document_symbol.cr b/src/ls/document_symbol.cr index fc862c8d9..5e2c7a1a9 100644 --- a/src/ls/document_symbol.cr +++ b/src/ls/document_symbol.cr @@ -63,7 +63,7 @@ module Mint def location_from_node(node : Ast::Node) LSP::Location.new( - uri: "file://#{node.file.path}", + uri: "file://#{node.file.path.to_lsp_path}", range: LSP::Range.new( start: LSP::Position.new(node.from.line - 1, node.from.column), end: LSP::Position.new(node.to.line - 1, node.to.column))) diff --git a/src/ls/hover.cr b/src/ls/hover.cr index 823976583..45455c1fd 100644 --- a/src/ls/hover.cr +++ b/src/ls/hover.cr @@ -45,7 +45,7 @@ module Mint # this could take a while because the workspace parses # and type checks all of its source files. workspace = - server.workspace(uri.path.to_s) + server.workspace(uri.file_path) contents = case type_checker = workspace.result.value diff --git a/src/ls/sandbox.cr b/src/ls/sandbox.cr index b8aac76fc..645553494 100644 --- a/src/ls/sandbox.cr +++ b/src/ls/sandbox.cr @@ -29,7 +29,7 @@ module Mint end def reset(files : Array(Tuple(String, String))) - Dir.glob("#{@directory}/**/*") do |file| + Dir.glob("#{@directory.to_posix}/**/*") do |file| next if file == Path[@directory, "mint.json"].to_s FileUtils.rm(file) end diff --git a/src/ls/semantic_tokens.cr b/src/ls/semantic_tokens.cr index eed885247..375d49202 100644 --- a/src/ls/semantic_tokens.cr +++ b/src/ls/semantic_tokens.cr @@ -7,7 +7,7 @@ module Mint def execute(server : LSP::Server) path = - URI.parse(params.text_document.uri).path.to_s + URI.parse(params.text_document.uri).file_path case ast = server.workspace(path).ast(path) when Ast diff --git a/src/lsp/protocol/text_document_identifier.cr b/src/lsp/protocol/text_document_identifier.cr index 6e5e04f07..70a8d0251 100644 --- a/src/lsp/protocol/text_document_identifier.cr +++ b/src/lsp/protocol/text_document_identifier.cr @@ -7,7 +7,7 @@ module LSP # Returns the path of the URI def path - URI.parse(uri).try(&.path).to_s + URI.parse(uri).try(&.file_path).to_s end end end diff --git a/src/lsp/protocol/text_document_item.cr b/src/lsp/protocol/text_document_item.cr index 2b34381ea..7ee6be691 100644 --- a/src/lsp/protocol/text_document_item.cr +++ b/src/lsp/protocol/text_document_item.cr @@ -21,7 +21,7 @@ module LSP # Returns the path of the URI def path - URI.parse(uri).try(&.path).to_s + URI.parse(uri).try(&.file_path).to_s end end end diff --git a/src/message.cr b/src/message.cr index c93d54da2..4f02a02d8 100644 --- a/src/message.cr +++ b/src/message.cr @@ -79,7 +79,7 @@ module Mint SnippetData.new( to: position.offset + [min, parser.word(position).to_s.size].max, - filename: parser.file.relative_path, + filename: parser.file.relative_path_posix, location: {position, position}, input: parser.file.contents, path: parser.file.path, @@ -91,13 +91,13 @@ module Mint SnippetData.new( to: value.position.offset + [min, value.word.to_s.size].max, location: {value.position, value.position}, - filename: value.file.relative_path, + filename: value.file.relative_path_posix, from: value.position.offset, input: value.file.contents, path: value.file.path) in Ast::Node SnippetData.new( - filename: value.file.relative_path, + filename: value.file.relative_path_posix, location: {value.from, value.to}, input: value.file.contents, from: value.from.offset, diff --git a/src/parser/file.cr b/src/parser/file.cr index f7c0652f1..f8b767d9e 100644 --- a/src/parser/file.cr +++ b/src/parser/file.cr @@ -8,6 +8,10 @@ module Mint ::File.relative_path_from_ancestor(path, "mint.json") end + getter relative_path_posix : String do + Path[relative_path].to_posix.to_s + end + def initialize(@contents : String, @path : String) end diff --git a/src/test_runner/browser.cr b/src/test_runner/browser.cr index 55da777c1..57bb925b6 100644 --- a/src/test_runner/browser.cr +++ b/src/test_runner/browser.cr @@ -7,13 +7,17 @@ module Mint BROWSER_PATHS = { firefox: [ "/Applications/Firefox.app/Contents/MacOS/firefox-bin", + "C:\\Program Files\\Mozilla Firefox\\firefox.exe", "firefox-bin", "firefox", ], chrome: [ + "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", "chromium-browser", "google-chrome", + "chrome.exe", "chromium", ], } @@ -94,7 +98,11 @@ module Mint # Chromium requires --no-sandbox when running as root (common in # CI / Docker environments). Without it the browser refuses to start. private def needs_no_sandbox? : Bool - LibC.getuid == 0 + {% if flag?(:windows) %} + true + {% else %} + LibC.getuid == 0 + {% end %} end def start(url, profile_directory) diff --git a/src/type_checkers/directives/asset.cr b/src/type_checkers/directives/asset.cr index 68c3737a9..970afbc83 100644 --- a/src/type_checkers/directives/asset.cr +++ b/src/type_checkers/directives/asset.cr @@ -2,7 +2,7 @@ module Mint class TypeChecker def check(node : Ast::Directives::Asset) : Checkable error! :asset_directive_expected_file do - snippet "The path specified for an asset directive does not exist:", node.relative_path + snippet "The path specified for an asset directive does not exist:", node.relative_path_posix snippet "The asset directive in question is here:", node end unless node.exists? diff --git a/src/type_checkers/directives/highlight_file.cr b/src/type_checkers/directives/highlight_file.cr index 2a70e1bda..86882d3fa 100644 --- a/src/type_checkers/directives/highlight_file.cr +++ b/src/type_checkers/directives/highlight_file.cr @@ -2,7 +2,7 @@ module Mint class TypeChecker def check(node : Ast::Directives::HighlightFile) : Checkable error! :highlight_file_directive_expected_file do - snippet "The path specified for an highlight file directive does not exist:", node.relative_path + snippet "The path specified for an highlight file directive does not exist:", node.relative_path_posix snippet "The highlight file directive in question is here:", node end unless node.exists? diff --git a/src/type_checkers/directives/inline.cr b/src/type_checkers/directives/inline.cr index 583b35d8b..f3e7cd6a9 100644 --- a/src/type_checkers/directives/inline.cr +++ b/src/type_checkers/directives/inline.cr @@ -2,7 +2,7 @@ module Mint class TypeChecker def check(node : Ast::Directives::Inline) : Checkable error! :inline_directive_expected_file do - snippet "The path specified for an inline directive does not exist:", node.relative_path + snippet "The path specified for an inline directive does not exist:", node.relative_path_posix snippet "The inline directive in question is here:", node end unless node.exists? diff --git a/src/type_checkers/directives/svg.cr b/src/type_checkers/directives/svg.cr index d36e6cfa2..4ecefd31d 100644 --- a/src/type_checkers/directives/svg.cr +++ b/src/type_checkers/directives/svg.cr @@ -2,7 +2,7 @@ module Mint class TypeChecker def check(node : Ast::Directives::Svg) : Checkable error! :svg_directive_expected_file do - snippet "The specified file for an svg directive does not exist:", node.relative_path + snippet "The specified file for an svg directive does not exist:", node.relative_path_posix snippet "The svg directive in question is here:", node end unless node.exists? diff --git a/src/utils/icon_generator.cr b/src/utils/icon_generator.cr index 07fecd4f2..50f4db8ee 100644 --- a/src/utils/icon_generator.cr +++ b/src/utils/icon_generator.cr @@ -2,7 +2,34 @@ module Mint module IconGenerator extend self + @@executable : String? + @@searched = false + + # ImageMagick 7 provides the `magick` command while ImageMagick 6 only + # provides `convert`, so `magick` is preferred. + # + # On Windows `convert.exe` is also the name of a built in utility (it + # converts FAT volumes to NTFS) which lives in the system directory and + # would be found on every machine, so it is ignored. + def executable : String? + unless @@searched + @@searched = true + + @@executable = + case + when path = Process.find_executable("magick") + path + when path = Process.find_executable("convert") + path unless system_utility?(path) + end + end + + @@executable + end + def convert(image, size) + return "" unless command = executable + output = IO::Memory.new @@ -11,8 +38,9 @@ module Mint status = Process.run( - "convert #{image} -resize #{size}x#{size} -", - shell: true, error: error, output: output) + command, + args: [image, "-resize", "#{size}x#{size}", "png:-"], + error: error, output: output) if status.success? output.to_s @@ -20,5 +48,16 @@ module Mint "" end end + + private def system_utility?(path : String) : Bool + {% if flag?(:windows) %} + root = + Path[ENV["SystemRoot"]? || "C:\\Windows"].normalize.to_s + + Path[path].normalize.to_s.starts_with?("#{root}/") + {% else %} + false + {% end %} + end end end diff --git a/src/utils/source_files.cr b/src/utils/source_files.cr index 025b815ee..4ef77a8ae 100644 --- a/src/utils/source_files.cr +++ b/src/utils/source_files.cr @@ -16,8 +16,8 @@ module Mint def everything(json : MintJson, *, include_tests = false, dot_env = ".env") : Array(String) packages(json, include_self: true) - .flat_map { |item| globs(item, include_tests: include_tests) + [item.path] } - .push(Path[dot_env].to_s) + .flat_map { |item| globs(item, include_tests: include_tests) + [Path[item.path].to_posix.to_s] } + .push(Path[dot_env].to_posix.to_s) end def packages(json : MintJson, *, include_self = false) : Array(MintJson) From e096cddd276987f415b418642df67ab1021b3367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Fri, 28 Aug 2026 16:05:26 +0200 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Sijawusz Pur Rahnama --- spec/compilers_spec.cr | 2 +- src/ext/uri.cr | 2 +- src/test_runner/browser.cr | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/compilers_spec.cr b/spec/compilers_spec.cr index b2f38f55e..b746593f6 100644 --- a/spec/compilers_spec.cr +++ b/spec/compilers_spec.cr @@ -31,7 +31,7 @@ Dir end file_path = - Path[File.dirname(__FILE__), "../", file].normalize.to_s + Path[__DIR__, "..", file].normalize.to_s # Parse the sample ast = Mint::Parser.parse(sample, file_path) diff --git a/src/ext/uri.cr b/src/ext/uri.cr index 81b276120..2802cc029 100644 --- a/src/ext/uri.cr +++ b/src/ext/uri.cr @@ -6,7 +6,7 @@ class URI path = URI.decode(self.path.lchop("/")) - if match = /([A-Z]):\/(.*)/i.match(path) + if match = /([A-Z]):\/(.*)/i.match_full(path) "#{match[1].upcase}:\\#{match[2].gsub('/', '\\')}" else self.path diff --git a/src/test_runner/browser.cr b/src/test_runner/browser.cr index 57bb925b6..c726340a8 100644 --- a/src/test_runner/browser.cr +++ b/src/test_runner/browser.cr @@ -12,9 +12,9 @@ module Mint "firefox", ], chrome: [ - "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", + "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", "chromium-browser", "google-chrome", "chrome.exe", From 10388e05a4947b6140c9db546d7f504a3f22db1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Fri, 28 Aug 2026 15:09:22 +0100 Subject: [PATCH 3/3] Update uri.cr --- src/ext/uri.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ext/uri.cr b/src/ext/uri.cr index 2802cc029..08716f242 100644 --- a/src/ext/uri.cr +++ b/src/ext/uri.cr @@ -6,7 +6,7 @@ class URI path = URI.decode(self.path.lchop("/")) - if match = /([A-Z]):\/(.*)/i.match_full(path) + if match = path.match_full(/([A-Z]):\/(.*)/i) "#{match[1].upcase}:\\#{match[2].gsub('/', '\\')}" else self.path