diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72287983..4f562a80 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,73 +7,64 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-20.04, macos-latest] + os: [windows-latest] + defaults: + run: + shell: bash steps: - - uses: actions/setup-node@v2 - with: - node-version: '18' - - uses: actions/checkout@v2.3.2 + - uses: actions/checkout@v2.3.2 + - run: rm -rf $(which pkg-config) - - name: Install esy - run: npm install -g esy + - uses: actions/setup-node@v4 + with: + node-version: "20" - - name: Try to restore install cache - uses: actions/cache@v2.1.1 + - uses: lukka/get-cmake@latest + - name: Setup anew (or from cache) vcpkg (and does not build any package) + uses: lukka/run-vcpkg@v11 + env: + VCPKG_DEFAULT_TRIPLET: x64-mingw-static with: - path: ~/.esy/source - key: source-${{ hashFiles('**/index.json') }} + runVcpkgInstall: true + runVcpkgFormatString: '["install", "--clean-after-build"]' + + - run: vcpkg install + + - name: Set pkg-config path on Unix + if: runner.os != 'Windows' + run: | + ls "${GITHUB_WORKSPACE}/vcpkg_installed/${VCPKG_DEFAULT_TRIPLET}/lib/pkgconfig" + echo "PKG_CONFIG_PATH=${GITHUB_WORKSPACE}/vcpkg_installed/${VCPKG_DEFAULT_TRIPLET}/lib/pkgconfig" >> $GITHUB_ENV + + - name: Set pkg-config path on Unix + if: runner.os == 'Windows' + shell: bash + run: | + echo "PKG_CONFIG_PATH=${GITHUB_WORKSPACE}\vcpkg_installed\\${VCPKG_DEFAULT_TRIPLET}\lib\pkgconfig" >> $GITHUB_ENV - - name: Install - run: esy install + - shell: cmd + run: tree D:\a\odiff\odiff\vcpkg_installed\x64-mingw-static - - name: Print esy cache - uses: actions/github-script@v3.0.0 - id: print_esy_cache + - name: Install winget + uses: Cyberboss/install-winget@v1 with: - script: | - const path = require('path') - const scriptPath = path.resolve('.github/workflows/print_esy_cache.js') - require(scriptPath)(core) - - - name: Try to restore build cache - id: deps-cache - uses: actions/cache@v2.1.1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - shell: bash + run: | + winget install --id=bloodrock.pkg-config-lite -e --disable-interactivity --accept-source-agreements + + - run: pkg-config --cflags --libs libspng + + - uses: ocaml/setup-ocaml@v3 with: - path: ${{ steps.print_esy_cache.outputs.esy_cache }} - key: build-odiff-${{ matrix.os }}-${{ hashFiles('**/index.json') }} - restore-keys: build-odiff-${{ matrix.os }} - - # Here we use a low-level command. In real situation you don't have to - # but it is useful in CI as it split the log in GitHub UI. - # You can see at a glance if it is your project or your deps that break. - # - # We also use --release flag to build less. - # This allow us to spot syntax/type error more quickly. - - name: Build release dependencies - - if: steps.deps-cache.outputs.cache-hit != 'true' - run: esy build-dependencies --release - - - name: Build project in release - run: esy build --release - - # Now that our core project build let builds others deps - - name: Build dependencies - if: steps.deps-cache.outputs.cache-hit != 'true' - run: esy build-dependencies - - - name: Build project - run: esy build - - - name: Ensure readme is up-to-date - run: node scripts/process-readme.js verify - - # Here we cleanup if we have a cache fail because we use restore-keys. - # restore-keys take the old store even on cache fail. - # So, we have deps we don't care anymore. We prune them. - - name: Clean global store - if: steps.deps-cache.outputs.cache-hit != 'true' - run: esy cleanup . + ocaml-compiler: 4.14.0 + dune-cache: true + + - name: Install deps & build + run: opam install . --with-test + + - run: opam exec -- dune build --verbose - name: Test run: esy test diff --git a/.gitignore b/.gitignore index 8b76fc71..635d699e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ _release *.install images/diff.png test/test-images/_*.png - +vcpkg_installed/* +_opam/ diff --git a/bin/ODiffBin.ml b/bin/ODiffBin.ml index f497176d..c46c98dc 100644 --- a/bin/ODiffBin.ml +++ b/bin/ODiffBin.ml @@ -74,22 +74,26 @@ let ignoreRegions = \"x1:y1-x2:y2\". Multiple regions are separated with a ','." let cmd = + const Main.main $ base $ comp $ diffPath $ threshold $ diffMask $ failOnLayout + $ diffColor $ parsableOutput $ antialiasing $ ignoreRegions $ diffLines + $ disableGcOptimizations + +let info = let man = [ `S Manpage.s_description; `P "$(tname) is the fastest pixel-by-pixel image comparison tool."; - `P "Supported image types: .png, .jpg, .jpeg, .bitmap"; + `P "Supported image types: .png, .jpg, .jpeg, .tiff"; ] in - ( const Main.main $ base $ comp $ diffPath $ threshold $ diffMask - $ failOnLayout $ diffColor $ parsableOutput $ antialiasing $ ignoreRegions - $ diffLines $ disableGcOptimizations, - Term.info "odiff" ~version:"3.0.1" ~doc:"Find difference between 2 images." - ~exits: - (Term.exit_info 0 ~doc:"on image match" - :: Term.exit_info 21 ~doc:"on layout diff when --fail-on-layout" - :: Term.exit_info 22 ~doc:"on image pixel difference" - :: Term.default_error_exits) - ~man ) + Cmd.info "odiff" ~version:"3.0.1" ~doc:"Find difference between 2 images." + ~exits: + [ + Cmd.Exit.info 0 ~doc:"on image match"; + Cmd.Exit.info 21 ~doc:"on layout diff when --fail-on-layout"; + Cmd.Exit.info 22 ~doc:"on image pixel difference"; + ] + ~man -let () = Term.eval cmd |> Term.exit +let cmd = Cmd.v info cmd +let () = Cmd.eval cmd |> Stdlib.exit diff --git a/bin/Print.ml b/bin/Print.ml index 239c37c9..f4992669 100644 --- a/bin/Print.ml +++ b/bin/Print.ml @@ -1,53 +1,53 @@ -open Odiff.Diff +(* open Odiff.Diff *) let printDiffResult makeParsableOutput result = - (match (result, makeParsableOutput) with - | Layout, true -> "" - | Layout, false -> - Pastel.createElement - ~children: - [ - (Pastel.createElement ~color:Red ~bold:true ~children:[ "Failure!" ] - () [@JSX]); - " Images have different layout.\n"; - ] - () [@JSX] - | Pixel (_output, diffCount, _percentage, _lines), true when diffCount == 0 -> - "" - | Pixel (_output, diffCount, _percentage, _lines), false when diffCount == 0 - -> - Pastel.createElement - ~children: - [ - (Pastel.createElement ~color:Green ~bold:true - ~children:[ "Success!" ] () [@JSX]); - " Images are equal.\n"; - (Pastel.createElement ~dim:true - ~children:[ "No diff output created." ] - () [@JSX]); - ] - () [@JSX] - | Pixel (_output, diffCount, diffPercentage, stack), true - when not (Stack.is_empty stack) -> - Int.to_string diffCount ^ ";" - ^ Float.to_string diffPercentage - ^ ";" - ^ (stack - |> Stack.fold (fun acc line -> (line |> Int.to_string) ^ "," ^ acc) "") - | Pixel (_output, diffCount, diffPercentage, _), true -> - Int.to_string diffCount ^ ";" ^ Float.to_string diffPercentage - | Pixel (_output, diffCount, diffPercentage, _lines), false -> - Pastel.createElement - ~children: - [ - (Pastel.createElement ~color:Red ~bold:true ~children:[ "Failure!" ] - () [@JSX]); - " Images are different.\n"; - "Different pixels: "; - (Pastel.createElement ~color:Red ~bold:true - ~children:[ Printf.sprintf "%i (%f%%)" diffCount diffPercentage ] - () [@JSX]); - ] - () [@JSX]) - |> Console.log; + (* (match (result, makeParsableOutput) with *) + (* | Layout, true -> "" *) + (* | Layout, false -> *) + (* Pastel.createElement *) + (* ~children: *) + (* [ *) + (* (Pastel.createElement ~color:Red ~bold:true ~children:[ "Failure!" ] *) + (* () [@JSX]); *) + (* " Images have different layout.\n"; *) + (* ] *) + (* () [@JSX] *) + (* | Pixel (_output, diffCount, _percentage, _lines), true when diffCount == 0 -> *) + (* "" *) + (* | Pixel (_output, diffCount, _percentage, _lines), false when diffCount == 0 *) + (* -> *) + (* Pastel.createElement *) + (* ~children: *) + (* [ *) + (* (Pastel.createElement ~color:Green ~bold:true *) + (* ~children:[ "Success!" ] () [@JSX]); *) + (* " Images are equal.\n"; *) + (* (Pastel.createElement ~dim:true *) + (* ~children:[ "No diff output created." ] *) + (* () [@JSX]); *) + (* ] *) + (* () [@JSX] *) + (* | Pixel (_output, diffCount, diffPercentage, stack), true *) + (* when not (Stack.is_empty stack) -> *) + (* Int.to_string diffCount ^ ";" *) + (* ^ Float.to_string diffPercentage *) + (* ^ ";" *) + (* ^ (stack *) + (* |> Stack.fold (fun acc line -> (line |> Int.to_string) ^ "," ^ acc) "") *) + (* | Pixel (_output, diffCount, diffPercentage, _), true -> *) + (* Int.to_string diffCount ^ ";" ^ Float.to_string diffPercentage *) + (* | Pixel (_output, diffCount, diffPercentage, _lines), false -> *) + (* Pastel.createElement *) + (* ~children: *) + (* [ *) + (* (Pastel.createElement ~color:Red ~bold:true ~children:[ "Failure!" ] *) + (* () [@JSX]); *) + (* " Images are different.\n"; *) + (* "Different pixels: "; *) + (* (Pastel.createElement ~color:Red ~bold:true *) + (* ~children:[ Printf.sprintf "%i (%f%%)" diffCount diffPercentage ] *) + (* () [@JSX]); *) + (* ] *) + (* () [@JSX]) *) + (* |> Console.log; *) result diff --git a/bin/dune b/bin/dune index 78ee6dc9..d5c72b2b 100644 --- a/bin/dune +++ b/bin/dune @@ -4,4 +4,4 @@ (package odiff) (flags (:standard -w -27)) - (libraries console.lib pastel.lib odiff-core odiff-io cmdliner)) + (libraries odiff-core odiff-io cmdliner)) diff --git a/dune-project b/dune-project index 162f4838..198e9ce3 100644 --- a/dune-project +++ b/dune-project @@ -1,4 +1,5 @@ (lang dune 2.8) +(name odiff) ; Warning: The flag set for these foreign sources overrides the `:standard` set ; of flags. However the flags in this standard set are still added to the @@ -18,9 +19,11 @@ (package (name odiff) + (synopsis "CLI for comparing images pixel-by-pixel") (depends odiff-core odiff-io + (cmdliner (= 1.3.0)) ) ) @@ -29,8 +32,7 @@ (synopsis "Pixel-by-pixel image difference algorithm") (depends dune - (reason (and (>= 3.6.0) (< 4.0.0))) - (ocaml (and (>= 4.10.0) (< 5.0.0))) + (ocaml (= 4.14.0)) ) ) @@ -39,9 +41,8 @@ (synopsis "Ready to use io for odiff-core") (depends dune - conf-libpng odiff-core - (reason (and (>= 3.6.0) (< 4.0.0))) - (ocaml (and (>= 4.10.0) (< 4.11.0))) + (ocaml (= 4.14.0)) + (dune-configurator (>= 2.8)) ) ) diff --git a/images/out.png b/images/out.png new file mode 100644 index 00000000..5ec21163 Binary files /dev/null and b/images/out.png differ diff --git a/io/config/discover.ml b/io/config/discover.ml index da8da519..4c1cc691 100644 --- a/io/config/discover.ml +++ b/io/config/discover.ml @@ -1,27 +1,123 @@ module C = Configurator.V1 -let _ = - C.main ~name:"odiff-c-lib-package-resolver" (fun _c -> - let spng_include_path = Sys.getenv "SPNG_INCLUDE_PATH" |> String.trim in - let spng_lib_path = Sys.getenv "SPNG_LIB_PATH" |> String.trim in - let libspng = spng_lib_path ^ "/libspng_static.a" in - let jpeg_include_path = Sys.getenv "JPEG_INCLUDE_PATH" |> String.trim in - let jpeg_lib_path = Sys.getenv "JPEG_LIB_PATH" |> String.trim in - let libjpeg = jpeg_lib_path ^ "/libjpeg.a" in - let tiff_include_path = Sys.getenv "TIFF_INCLUDE_PATH" |> String.trim in - let tiff_lib_path = Sys.getenv "TIFF_LIB_PATH" |> String.trim in - let libtiff = tiff_lib_path ^ "/libtiff.a" in - let z_lib_path = Sys.getenv "Z_LIB_PATH" |> String.trim in - let zlib = z_lib_path ^ "/libz.a" in - C.Flags.write_sexp "png_write_c_flags.sexp" [ "-I" ^ spng_include_path ]; - C.Flags.write_sexp "png_write_c_library_flags.sexp" [ libspng; zlib ]; - C.Flags.write_sexp "png_write_flags.sexp" [ "-cclib"; libspng ]; - C.Flags.write_sexp "png_c_flags.sexp" [ "-I" ^ spng_include_path ]; - C.Flags.write_sexp "png_c_library_flags.sexp" [ libspng; zlib ]; - C.Flags.write_sexp "png_flags.sexp" [ "-cclib"; libspng ]; - C.Flags.write_sexp "jpg_c_flags.sexp" [ "-I" ^ jpeg_include_path ]; - C.Flags.write_sexp "jpg_c_library_flags.sexp" [ libjpeg ]; - C.Flags.write_sexp "jpg_flags.sexp" [ "-cclib"; libjpeg ]; - C.Flags.write_sexp "tiff_c_flags.sexp" [ "-I" ^ tiff_include_path ]; - C.Flags.write_sexp "tiff_c_library_flags.sexp" [ libtiff; zlib ]; - C.Flags.write_sexp "tiff_flags.sexp" [ "-cclib"; libtiff ]) +exception Pkg_Config_Resolution_Failed of string + +type pkg_config_result = { cflags : string; libs : string } +type process_result = { exit_code : int; stdout : string; stderr : string } + +let run_process ~env prog args = + let stdout_fn = Filename.temp_file "stdout" ".tmp" in + let stderr_fn = Filename.temp_file "stderr" ".tmp" in + let openfile f = + Unix.openfile f [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o666 + in + let stdout = openfile stdout_fn in + let stderr = openfile stderr_fn in + let stdin, stdin_w = Unix.pipe () in + Unix.close stdin_w; + + let pid = + match env with + | [] -> + Unix.create_process prog + (Array.of_list (prog :: args)) + stdin stdout stderr + | _ -> + let env_array = Array.of_list env in + Unix.create_process_env prog + (Array.of_list (prog :: args)) + env_array stdin stdout stderr + in + + Unix.close stdin; + Unix.close stdout; + Unix.close stderr; + + let _, status = Unix.waitpid [] pid in + + let read_file filename = + try + let ic = open_in filename in + let n = in_channel_length ic in + let s = really_input_string ic n in + close_in ic; + s + with + | Sys_error msg -> Printf.sprintf "Error reading file %s: %s" filename msg + | End_of_file -> + Printf.sprintf "Unexpected end of file while reading %s" filename + in + + let stdout_content = read_file stdout_fn in + let stderr_content = read_file stderr_fn in + + Sys.remove stdout_fn; + Sys.remove stderr_fn; + + let exit_code = + match status with + | Unix.WEXITED code -> code + | Unix.WSIGNALED signal -> + raise + (Pkg_Config_Resolution_Failed + (Printf.sprintf "Process killed by signal %d" signal)) + | Unix.WSTOPPED signal -> + raise + (Pkg_Config_Resolution_Failed + (Printf.sprintf "Process stopped by signal %d" signal)) + in + + { exit_code; stdout = stdout_content; stderr = stderr_content } + +let run_pkg_config _c lib = + let pkg_config_path = Sys.getenv "PKG_CONFIG_PATH" in + Printf.printf "Use PKG_CONFIG_PATH: %s\n" pkg_config_path; + + let env = [ "PKG_CONFIG_PATH=" ^ pkg_config_path ] in + let c_flags_result = run_process ~env "pkg-config" [ "--cflags"; lib ] in + let libs_result = run_process ~env "pkg-config" [ "--libs"; lib ] in + + if c_flags_result.exit_code = 0 && libs_result.exit_code == 0 then + { cflags = c_flags_result.stdout; libs = libs_result.stdout } + else + let std_errors = + String.concat "\n" [ c_flags_result.stderr; libs_result.stderr ] + in + + raise (Pkg_Config_Resolution_Failed std_errors) + +let get_flags_from_env_or_run_pkg_conifg c ~env ~lib = + match (Sys.getenv_opt (env ^ "_CFLAGS"), Sys.getenv_opt (env ^ "_LIBS")) with + | Some cflags, Some lib -> + { cflags = String.trim cflags; libs = String.trim lib } + | None, None -> run_pkg_config c lib + | _ -> + let err = "Missing CFLAGS or LIB env vars for " ^ env in + raise (Pkg_Config_Resolution_Failed err) + +let () = + C.main ~name:"odiff-c-lib-packae-resolver" (fun c -> + let png_config = + get_flags_from_env_or_run_pkg_conifg c ~env:"LIBPNG" + ~lib:"libspng_static" + in + let tiff_config = + get_flags_from_env_or_run_pkg_conifg c ~lib:"libtiff-4" ~env:"LIBTIFF" + in + let jpeg_config = + get_flags_from_env_or_run_pkg_conifg c ~lib:"libturbojpeg" + ~env:"LIBJPEG" + in + + C.Flags.write_sexp "png_c_flags.sexp" [ png_config.cflags ]; + C.Flags.write_sexp "png_c_library_flags.sexp" [ png_config.libs ]; + C.Flags.write_sexp "png_write_c_flags.sexp" [ png_config.cflags ]; + C.Flags.write_sexp "png_write_c_library_flags.sexp" [ png_config.libs ]; + C.Flags.write_sexp "png_write_flags.sexp" [ "-cclib"; png_config.libs ]; + C.Flags.write_sexp "png_c_flags.sexp" [ png_config.cflags ]; + C.Flags.write_sexp "jpg_c_flags.sexp" [ jpeg_config.cflags ]; + C.Flags.write_sexp "jpg_c_library_flags.sexp" [ jpeg_config.libs ]; + C.Flags.write_sexp "jpg_flags.sexp" [ "-cclib"; jpeg_config.libs ]; + C.Flags.write_sexp "tiff_c_flags.sexp" [ tiff_config.cflags ]; + C.Flags.write_sexp "tiff_c_library_flags.sexp" [ tiff_config.libs ]; + C.Flags.write_sexp "tiff_flags.sexp" [ "-cclib"; tiff_config.libs ]) diff --git a/io/config/dune b/io/config/dune index bc2970ff..187bd5e1 100644 --- a/io/config/dune +++ b/io/config/dune @@ -1,5 +1,3 @@ (executable (name discover) - (ocamlc_flags str.cma) - (ocamlopt_flags str.cmxa) (libraries dune-configurator)) diff --git a/io/png/dune b/io/png/dune index a1746aad..e616eca9 100644 --- a/io/png/dune +++ b/io/png/dune @@ -14,6 +14,6 @@ (libraries odiff-core WritePng)) (rule - (targets png_flags.sexp png_c_flags.sexp png_c_library_flags.sexp) + (targets png_c_flags.sexp png_c_flags.sexp png_c_library_flags.sexp) (action (run ../config/discover.exe))) diff --git a/odiff-core.opam b/odiff-core.opam index 1abe43d4..c4b403bd 100644 --- a/odiff-core.opam +++ b/odiff-core.opam @@ -8,8 +8,7 @@ homepage: "https://github.com/dmtrKovalenko/odiff" bug-reports: "https://github.com/dmtrKovalenko/odiff/issues" depends: [ "dune" {>= "2.8"} - "reason" {>= "3.6.0" & < "4.0.0"} - "ocaml" {>= "4.10.0" & < "5.0.0"} + "ocaml" {= "4.14.0"} "odoc" {with-doc} ] build: [ diff --git a/odiff-io.opam b/odiff-io.opam index 79d1d3d0..1c10ae01 100644 --- a/odiff-io.opam +++ b/odiff-io.opam @@ -8,10 +8,9 @@ homepage: "https://github.com/dmtrKovalenko/odiff" bug-reports: "https://github.com/dmtrKovalenko/odiff/issues" depends: [ "dune" {>= "2.8"} - "conf-libpng" "odiff-core" - "reason" {>= "3.6.0" & < "4.0.0"} - "ocaml" {>= "4.10.0" & < "4.11.0"} + "ocaml" {= "4.14.0"} + "dune-configurator" {>= "2.8"} "odoc" {with-doc} ] build: [ diff --git a/odiff.opam b/odiff.opam index 1de5daad..2148379f 100644 --- a/odiff.opam +++ b/odiff.opam @@ -1,5 +1,6 @@ # This file is generated by dune, edit dune-project instead opam-version: "2.0" +synopsis: "CLI for comparing images pixel-by-pixel" maintainer: ["https://dmtrkovalenko.dev" "dmtr.kovalenko@outlook.com"] authors: ["Dmitriy Kovalenko"] license: "MIT" @@ -9,6 +10,7 @@ depends: [ "dune" {>= "2.8"} "odiff-core" "odiff-io" + "cmdliner" {= "1.3.0"} "odoc" {with-doc} ] build: [ diff --git a/out.png b/out.png new file mode 100644 index 00000000..5ec21163 Binary files /dev/null and b/out.png differ diff --git a/test/dune b/test/_dune similarity index 100% rename from test/dune rename to test/_dune diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 00000000..9407f7ef --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "builtin-baseline": "fe1cde61e971d53c9687cf9a46308f8f55da19fa", + "dependencies": ["libspng", "tiff", "libjpeg-turbo"] +}