Packages

A CI/CD tool for checking dependency license compatibility in Elixir projects. Reads license information from local hex metadata files and validates compatibility with your project's license using industry-standard rules.

Current section

Files

Jump to
depscheck lib mix tasks depscheck.ex
Raw

lib/mix/tasks/depscheck.ex

defmodule Mix.Tasks.Depscheck do
@shortdoc "Checks dependency licenses for compatibility"
@moduledoc """
Checks dependency licenses for compatibility with your project license.
## Usage
mix depscheck
mix depscheck --verbose
## Configuration
Create a `.depscheck.exs` file in your project root:
%{
ignored_packages: ["some_package", "another_package"],
project_license: "All Rights Reserved" # Override project license
}
## Exit Codes
- 0: All dependencies are compatible
- 1: License violations found
"""
use Mix.Task
alias Depscheck.{Compatibility, Config, LicenseDetector}
@impl Mix.Task
def run(args) do
Mix.Task.run("loadpaths")
verbose? = "--verbose" in args || "-v" in args
config = Config.load()
project_license = LicenseDetector.get_project_license_with_config(config)
dependencies = LicenseDetector.get_all_dependency_licenses()
result = Compatibility.check_all(project_license, dependencies, config)
print_results(result, config, verbose?)
if result.status == :fail do
System.halt(1)
end
end
# Private functions
defp print_results(result, config, verbose?) do
ignored_set = MapSet.new(config.ignored_packages)
all_deps = LicenseDetector.get_all_dependency_licenses()
if verbose? do
print_verbose_results(result, ignored_set, all_deps)
else
print_minimal_results(result, ignored_set, all_deps)
end
end
defp print_verbose_results(result, ignored_set, all_deps) do
print_header(result)
Enum.each(all_deps, fn dep ->
cond do
MapSet.member?(ignored_set, dep.name) ->
print_ignored(dep)
dep in result.dependencies ->
print_dependency(dep, result)
true ->
# Shouldn't happen but handle gracefully
print_dependency(dep, result)
end
end)
print_warnings(result)
IO.puts("")
print_summary(result, all_deps)
IO.puts("")
end
defp print_minimal_results(result, ignored_set, all_deps) do
has_failures = result.status == :fail
has_warnings = !Enum.empty?(result.warnings)
if has_failures || has_warnings do
print_header(result)
end
print_violation_dependencies(result, ignored_set, all_deps)
print_warnings(result)
case result.status do
:pass ->
print_success_message(all_deps, has_warnings)
:fail ->
print_failure_summary(result)
end
end
defp print_dependency(dep, result) do
licenses_str = Enum.join(dep.licenses, ", ")
has_violation = Enum.any?(result.violations, &String.contains?(&1, dep.name))
if has_violation do
IO.puts(
IO.ANSI.red() <> "✗ #{dep.name} (#{licenses_str}) - INCOMPATIBLE" <> IO.ANSI.reset()
)
else
IO.puts(
IO.ANSI.green() <> "✓ #{dep.name} (#{licenses_str}) - Compatible" <> IO.ANSI.reset()
)
end
end
defp print_ignored(dep) do
licenses_str = Enum.join(dep.licenses, ", ")
IO.puts(IO.ANSI.yellow() <> "⊘ #{dep.name} (#{licenses_str}) - Ignored" <> IO.ANSI.reset())
end
defp print_header(result) do
project_name = Mix.Project.config()[:app] |> to_string() |> String.capitalize()
license_display = result.project_license || "No license"
IO.puts("\nChecking licenses for #{project_name} (#{license_display})...\n")
end
defp print_warnings(result) do
if !Enum.empty?(result.warnings) do
IO.puts("")
IO.puts(IO.ANSI.yellow() <> "Warnings:" <> IO.ANSI.reset())
Enum.each(result.warnings, fn warning ->
IO.puts(IO.ANSI.yellow() <> " ⚠ #{warning}" <> IO.ANSI.reset())
end)
end
end
defp print_success_message(all_deps, include_blank_lines? \\ false) do
if include_blank_lines? do
IO.puts("")
end
message =
IO.ANSI.green() <>
"✓ All #{length(all_deps)} dependencies are compatible" <> IO.ANSI.reset()
IO.puts(message)
if include_blank_lines? do
IO.puts("")
end
end
defp print_failure_summary(result) do
violation_count = length(result.violations)
IO.puts("")
IO.puts(
IO.ANSI.red() <>
"✗ Found #{violation_count} license violation(s)" <> IO.ANSI.reset()
)
IO.puts("\nViolations:")
Enum.each(result.violations, fn violation ->
IO.puts(IO.ANSI.red() <> " • #{violation}" <> IO.ANSI.reset())
end)
IO.puts("")
end
defp print_summary(%{status: :pass}, all_deps) do
print_success_message(all_deps)
end
defp print_summary(%{status: :fail}, all_deps) do
print_failure_summary(all_deps)
end
defp print_violation_dependencies(result, ignored_set, all_deps) do
all_deps
|> Enum.filter(fn dep ->
if MapSet.member?(ignored_set, dep.name) do
false
else
Enum.any?(result.violations, &String.contains?(&1, dep.name))
end
end)
|> Enum.each(fn dep ->
print_dependency(dep, result)
end)
end
end