feat: build infrastructure
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -21,16 +21,24 @@ defmodule BDS.MCP.AgentConfig do
|
||||
def config_path(:claude_code, home_dir), do: Path.join(home_dir, ".claude.json")
|
||||
def config_path(:github_copilot, home_dir), do: Path.join([home_dir, "Library", "Application Support", "Code", "User", "mcp.json"])
|
||||
|
||||
def packaged_executable_path(install_root, platform) when is_binary(install_root) do
|
||||
executable_name =
|
||||
case normalize_platform(platform) do
|
||||
:windows -> "bds-mcp.bat"
|
||||
_other -> "bds-mcp"
|
||||
end
|
||||
|
||||
Path.join([install_root, "mcp", "bin", executable_name])
|
||||
end
|
||||
|
||||
defp default_command(opts) do
|
||||
Keyword.get(opts, :script_path, repo_script_path())
|
||||
install_root = Keyword.fetch!(opts, :install_root)
|
||||
platform = Keyword.get(opts, :platform, current_platform())
|
||||
packaged_executable_path(install_root, platform)
|
||||
end
|
||||
|
||||
defp default_args(_opts), do: []
|
||||
|
||||
defp repo_script_path do
|
||||
Path.expand("../../../bin/bds-mcp", __DIR__)
|
||||
end
|
||||
|
||||
defp read_config(path) do
|
||||
if File.exists?(path) do
|
||||
path
|
||||
@@ -51,4 +59,15 @@ defmodule BDS.MCP.AgentConfig do
|
||||
servers = Map.get(config, "mcpServers", %{})
|
||||
Map.put(config, "mcpServers", Map.put(servers, @server_name, %{"command" => command, "args" => args}))
|
||||
end
|
||||
|
||||
defp current_platform do
|
||||
case :os.type() do
|
||||
{:win32, _type} -> :windows
|
||||
{:unix, :darwin} -> :macos
|
||||
{:unix, _type} -> :linux
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_platform(:darwin), do: :macos
|
||||
defp normalize_platform(platform), do: platform
|
||||
end
|
||||
|
||||
132
lib/bds/release_packaging.ex
Normal file
132
lib/bds/release_packaging.ex
Normal file
@@ -0,0 +1,132 @@
|
||||
defmodule BDS.ReleasePackaging do
|
||||
@moduledoc false
|
||||
|
||||
defmodule Metadata do
|
||||
@enforce_keys [
|
||||
:platform,
|
||||
:version,
|
||||
:output_dir,
|
||||
:payload_name,
|
||||
:payload_root,
|
||||
:app_root,
|
||||
:resources_root,
|
||||
:mcp_root,
|
||||
:archive_path
|
||||
]
|
||||
defstruct [
|
||||
:platform,
|
||||
:version,
|
||||
:output_dir,
|
||||
:payload_name,
|
||||
:payload_root,
|
||||
:app_root,
|
||||
:resources_root,
|
||||
:mcp_root,
|
||||
:archive_path
|
||||
]
|
||||
end
|
||||
|
||||
def build_metadata(platform, version, output_dir) when is_binary(version) and is_binary(output_dir) do
|
||||
normalized_platform = normalize_platform(platform)
|
||||
payload_name = "bds2-#{normalized_platform}-#{version}"
|
||||
payload_root = Path.join(output_dir, payload_name)
|
||||
|
||||
%Metadata{
|
||||
platform: normalized_platform,
|
||||
version: version,
|
||||
output_dir: output_dir,
|
||||
payload_name: payload_name,
|
||||
payload_root: payload_root,
|
||||
app_root: Path.join(payload_root, "app"),
|
||||
resources_root: Path.join(payload_root, "resources"),
|
||||
mcp_root: Path.join(payload_root, "resources/mcp"),
|
||||
archive_path: Path.join(output_dir, payload_name <> archive_extension(normalized_platform))
|
||||
}
|
||||
end
|
||||
|
||||
def package(opts) when is_list(opts) do
|
||||
metadata =
|
||||
build_metadata(
|
||||
Keyword.fetch!(opts, :platform),
|
||||
Keyword.fetch!(opts, :version),
|
||||
Keyword.fetch!(opts, :output_dir)
|
||||
)
|
||||
|
||||
app_release_source = Keyword.fetch!(opts, :app_release_source)
|
||||
mcp_release_source = Keyword.fetch!(opts, :mcp_release_source)
|
||||
|
||||
with :ok <- reset_output(metadata),
|
||||
:ok <- copy_release(app_release_source, metadata.app_root),
|
||||
:ok <- copy_release(mcp_release_source, metadata.resources_root),
|
||||
:ok <- write_manifest(metadata),
|
||||
:ok <- create_archive(metadata) do
|
||||
{:ok, metadata}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_platform(platform) when platform in [:macos, :linux, :windows], do: platform
|
||||
defp normalize_platform(:darwin), do: :macos
|
||||
defp normalize_platform(platform) when is_binary(platform), do: platform |> String.downcase() |> String.to_atom()
|
||||
|
||||
defp archive_extension(:windows), do: ".zip"
|
||||
defp archive_extension(_platform), do: ".tar.gz"
|
||||
|
||||
defp reset_output(metadata) do
|
||||
File.rm_rf!(metadata.payload_root)
|
||||
File.rm_rf!(metadata.archive_path)
|
||||
File.mkdir_p!(metadata.output_dir)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp copy_release(source, destination) do
|
||||
File.mkdir_p!(Path.dirname(destination))
|
||||
|
||||
case File.cp_r(source, destination) do
|
||||
{:ok, _files} -> :ok
|
||||
{:error, reason, _file} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp write_manifest(metadata) do
|
||||
manifest = %{
|
||||
"platform" => Atom.to_string(metadata.platform),
|
||||
"version" => metadata.version,
|
||||
"layout" => %{
|
||||
"app" => "app",
|
||||
"resources" => "resources",
|
||||
"mcp" => "resources/mcp"
|
||||
}
|
||||
}
|
||||
|
||||
manifest_path = Path.join(metadata.payload_root, "manifest.json")
|
||||
File.write!(manifest_path, Jason.encode!(manifest, pretty: true))
|
||||
:ok
|
||||
end
|
||||
|
||||
defp create_archive(%Metadata{platform: :windows} = metadata) do
|
||||
relative_entries = collect_entries(metadata.payload_root)
|
||||
cwd = metadata.output_dir |> String.to_charlist()
|
||||
archive = metadata.archive_path |> String.to_charlist()
|
||||
entries = Enum.map(relative_entries, &String.to_charlist(Path.join(metadata.payload_name, &1)))
|
||||
|
||||
case :zip.create(archive, entries, cwd: cwd) do
|
||||
{:ok, _archive_path} -> :ok
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp create_archive(metadata) do
|
||||
case System.cmd("tar", ["-czf", metadata.archive_path, "-C", metadata.output_dir, metadata.payload_name]) do
|
||||
{_output, 0} -> :ok
|
||||
{output, status} -> {:error, {:tar_failed, status, output}}
|
||||
end
|
||||
end
|
||||
|
||||
defp collect_entries(root) do
|
||||
root
|
||||
|> Path.join("**/*")
|
||||
|> Path.wildcard(match_dot: true)
|
||||
|> Enum.reject(&File.dir?/1)
|
||||
|> Enum.map(&Path.relative_to(&1, root))
|
||||
end
|
||||
end
|
||||
49
lib/mix/tasks/bds.package.ex
Normal file
49
lib/mix/tasks/bds.package.ex
Normal file
@@ -0,0 +1,49 @@
|
||||
defmodule Mix.Tasks.Bds.Package do
|
||||
@moduledoc false
|
||||
|
||||
use Mix.Task
|
||||
|
||||
@shortdoc "Packages built releases into a redistributable platform payload"
|
||||
|
||||
@impl Mix.Task
|
||||
def run(args) do
|
||||
{opts, positional, _invalid} =
|
||||
OptionParser.parse(args,
|
||||
strict: [output: :string, version: :string, app_release: :string, mcp_release: :string]
|
||||
)
|
||||
|
||||
platform = positional |> List.first() |> normalize_platform_arg()
|
||||
version = opts[:version] || Mix.Project.config()[:version]
|
||||
env_name = Atom.to_string(Mix.env())
|
||||
|
||||
package_opts = [
|
||||
platform: platform,
|
||||
version: version,
|
||||
output_dir: opts[:output] || Path.expand("dist/#{platform}", File.cwd!()),
|
||||
app_release_source: opts[:app_release] || Path.expand("_build/#{env_name}/rel/bds", File.cwd!()),
|
||||
mcp_release_source: opts[:mcp_release] || Path.expand("_build/#{env_name}/rel/bds_mcp", File.cwd!())
|
||||
]
|
||||
|
||||
case BDS.ReleasePackaging.package(package_opts) do
|
||||
{:ok, metadata} ->
|
||||
Mix.shell().info("Packaged #{metadata.payload_name} at #{metadata.archive_path}")
|
||||
|
||||
{:error, reason} ->
|
||||
Mix.raise("Packaging failed: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_platform_arg(nil), do: current_platform()
|
||||
defp normalize_platform_arg("macos"), do: :macos
|
||||
defp normalize_platform_arg("linux"), do: :linux
|
||||
defp normalize_platform_arg("windows"), do: :windows
|
||||
defp normalize_platform_arg(other), do: Mix.raise("Unsupported platform #{inspect(other)}")
|
||||
|
||||
defp current_platform do
|
||||
case :os.type() do
|
||||
{:win32, _type} -> :windows
|
||||
{:unix, :darwin} -> :macos
|
||||
{:unix, _type} -> :linux
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user