Skip to main content
Glama
erlang_release.bzl11 kB
# Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under both the MIT license found in the # LICENSE-MIT file in the root directory of this source tree and the Apache # License, Version 2.0 found in the LICENSE-APACHE file in the root directory # of this source tree. load("@prelude//:paths.bzl", "paths") load( ":erlang_application.bzl", "StartDependencySet", "StartSpec", "StartType", "build_apps_start_dependencies", ) load(":erlang_build.bzl", "erlang_build") load(":erlang_dependencies.bzl", "ErlAppDependencies", "flatten_dependencies") load( ":erlang_info.bzl", "ErlangAppInfo", "ErlangReleaseInfo", "ErlangToolchainInfo", ) load( ":erlang_toolchain.bzl", "Toolchain", # @unused Used as type "get_primary", "select_toolchains", ) load(":erlang_utils.bzl", "action_identifier") # Erlang Releases according to https://www.erlang.org/doc/design_principles/release_structure.html def erlang_release_impl(ctx: AnalysisContext) -> list[Provider]: all_apps = flatten_dependencies(ctx, _dependencies(ctx)) if ctx.attrs.multi_toolchain != None: return _build_multi_toolchain_releases(ctx, all_apps, ctx.attrs.multi_toolchain) else: return _build_primary_release(ctx, all_apps) def _build_multi_toolchain_releases( ctx: AnalysisContext, apps: ErlAppDependencies, configured_toolchains: list[Dependency]) -> list[Provider]: """build the release for all toolchains with the structure being releases/<toolchain>/<relname>""" all_toolchains = select_toolchains(ctx) toolchains = _get_configured_toolchains(all_toolchains, configured_toolchains) outputs = {} for toolchain in toolchains.values(): outputs[toolchain.name] = _build_release(ctx, toolchain, apps) releases_dir = _symlink_multi_toolchain_output(ctx, outputs) return [DefaultInfo(default_output = releases_dir), ErlangReleaseInfo(name = _relname(ctx))] def _get_configured_toolchains( toolchains: dict[str, Toolchain], configured_toolchains: list[Dependency]) -> dict[str, Toolchain]: retval = {} for dep in configured_toolchains: if not dep[ErlangToolchainInfo]: fail("{} is not a valid toolchain target".format(dep)) toolchain_info = dep[ErlangToolchainInfo] retval[toolchain_info.name] = toolchains[toolchain_info.name] return retval def _build_primary_release(ctx: AnalysisContext, apps: ErlAppDependencies) -> list[Provider]: """build the release only with the primary toolchain with the release folder on the top-level""" toolchains = select_toolchains(ctx) primary_toolchain = toolchains[get_primary(ctx)] all_outputs = _build_release(ctx, primary_toolchain, apps) release_dir = _symlink_primary_toolchain_output(ctx, all_outputs) return [DefaultInfo(default_output = release_dir), ErlangReleaseInfo(name = _relname(ctx))] def _build_release(ctx: AnalysisContext, toolchain: Toolchain, apps: ErlAppDependencies) -> dict[str, Artifact]: # OTP base structure lib_dir = build_lib_dir(ctx, toolchain, apps) boot_scripts = _build_boot_script(ctx, toolchain, lib_dir["lib"]) # release specific variables in bin/release_variables release_variables = _build_release_variables(ctx, toolchain) # Overlays overlays = _build_overlays(ctx) # erts maybe_erts = _build_erts(ctx, toolchain) # link output all_outputs = {} for outputs in [ lib_dir, boot_scripts, overlays, release_variables, maybe_erts, ]: all_outputs.update(outputs) return all_outputs def build_lib_dir( ctx: AnalysisContext, toolchain: Toolchain, all_apps: ErlAppDependencies) -> dict[str, Artifact]: """Build lib dir according to OTP specifications. .. seealso:: `OTP Design Principles Release Structure <https://www.erlang.org/doc/design_principles/release_structure.html>`_ """ build_dir = erlang_build.utils.build_dir(toolchain) link_spec = { _app_folder(toolchain, dep): dep[ErlangAppInfo].app_folders[toolchain.name] for dep in all_apps.values() if ErlangAppInfo in dep and not dep[ErlangAppInfo].virtual } lib_dir = ctx.actions.symlinked_dir( paths.join(build_dir, "lib"), link_spec, ) return {"lib": lib_dir} def _build_boot_script( ctx: AnalysisContext, toolchain: Toolchain, lib_dir: Artifact) -> dict[str, Artifact]: """Build Name.rel, start.script, and start.boot in the release folder.""" release_name = _relname(ctx) build_dir = erlang_build.utils.build_dir(toolchain) start_type_mapping = _dependencies_with_start_types(ctx) root_apps = _dependencies(ctx) root_apps_names = [app[ErlangAppInfo].name for app in root_apps] root_apps_with_start_type = [ (app, start_type_mapping[_app_name(app)]) for app in root_apps ] start_dependencies = build_apps_start_dependencies(ctx, toolchain, root_apps_with_start_type) root_set = ctx.actions.tset( StartDependencySet, value = StartSpec( name = "__ignored__", version = ctx.attrs.version, start_type = StartType("permanent"), resolved = False, ), children = start_dependencies, ) reverse_start_order = list(root_set.traverse()) reverse_start_order.pop(0) seen = set() release_applications = [] root_apps_spec = {} for spec in reverse_start_order[::-1]: if spec.name in seen: continue seen.add(spec.name) app_spec = { "name": spec.name, "resolved": spec.resolved, "type": spec.start_type.value, "version": spec.version, } if spec.name in root_apps_names: root_apps_spec[spec.name] = app_spec else: release_applications.append(app_spec) release_applications = [root_apps_spec[app_name] for app_name in root_apps_names] + release_applications data = { "apps": release_applications, "lib_dir": lib_dir, "name": release_name, "version": ctx.attrs.version, } spec_file = ctx.actions.write_json(paths.join(build_dir, "boot_script_spec.json"), data, with_inputs = True) scripts_dir = ctx.actions.declare_output(build_dir, "scripts", dir = True) erlang_build.utils.run_with_env( ctx, toolchain, cmd_args(toolchain.boot_script_builder, spec_file, scripts_dir.as_output()), category = "build_boot_script", identifier = action_identifier(toolchain, release_name), ) return { paths.join("releases", ctx.attrs.version, file): scripts_dir.project(file) for file in [ "{}.rel".format(release_name), "start.script", "start.boot", ] } def _build_overlays(ctx: AnalysisContext) -> dict[str, Artifact]: installed = {} for target, deps in ctx.attrs.overlays.items(): for dep in deps: for artifact in dep[DefaultInfo].default_outputs + dep[DefaultInfo].other_outputs: link_path = paths.normalize(paths.join(target, artifact.basename)) if link_path in installed: fail("multiple overlays defined for the same location: %s" % (link_path,)) installed[link_path] = artifact return installed def _build_release_variables(ctx: AnalysisContext, toolchain: Toolchain) -> dict[str, Artifact]: release_name = _relname(ctx) short_path = paths.join("bin", "release_variables") release_variables = ctx.actions.declare_output( erlang_build.utils.build_dir(toolchain), "release_variables", ) spec_file = ctx.actions.write_json( paths.join(erlang_build.utils.build_dir(toolchain), "relvars.json"), { "REL_NAME": release_name, "REL_VSN": ctx.attrs.version, }, ) erlang_build.utils.run_with_env( ctx, toolchain, cmd_args(toolchain.release_variables_builder, spec_file, release_variables.as_output()), category = "build_release_variables", identifier = action_identifier(toolchain, release_name), ) return {short_path: release_variables} def _build_erts(ctx: AnalysisContext, toolchain: Toolchain) -> dict[str, Artifact]: if not ctx.attrs.include_erts: return {} release_name = _relname(ctx) short_path = "erts" erts_dir = paths.join( erlang_build.utils.build_dir(toolchain), release_name, short_path, ) output_artifact = ctx.actions.declare_output(erts_dir) erlang_build.utils.run_with_env( ctx, toolchain, cmd_args(toolchain.include_erts, output_artifact.as_output()), category = "include_erts", identifier = action_identifier(toolchain, release_name), ) return {short_path: output_artifact} def _symlink_multi_toolchain_output(ctx: AnalysisContext, toolchain_artifacts: dict[str, dict[str, Artifact]]) -> Artifact: link_spec = {} relname = _relname(ctx) for toolchain_name, artifacts in toolchain_artifacts.items(): prefix = paths.join(toolchain_name, relname) link_spec.update({ paths.join(prefix, path): artifact for path, artifact in artifacts.items() }) return ctx.actions.symlinked_dir( "releases", link_spec, ) def _symlink_primary_toolchain_output(ctx: AnalysisContext, artifacts: dict[str, Artifact]) -> Artifact: return ctx.actions.symlinked_dir( _relname(ctx), artifacts, ) def _relname(ctx: AnalysisContext) -> str: return ctx.attrs.release_name if ctx.attrs.release_name else ctx.attrs.name def _app_folder(toolchain: Toolchain, dep: Dependency) -> str: """Build folder names (i.e., name-version) from an Erlang Application dependency.""" return "%s-%s" % (dep[ErlangAppInfo].name, dep[ErlangAppInfo].version[toolchain.name]) def _dependencies(ctx: AnalysisContext) -> list[Dependency]: """Extract dependencies from `applications` field, order preserving""" deps = [] for dep in ctx.attrs.applications: if type(dep) == "tuple": deps.append(dep[0]) else: deps.append(dep) return deps def _dependencies_with_start_types(ctx: AnalysisContext) -> dict[str, StartType]: """Extract mapping from dependency to start type from `applications` field, this is not order preserving""" deps = {} for dep in ctx.attrs.applications: if type(dep) == "tuple": deps[_app_name(dep[0])] = StartType(dep[1]) else: deps[_app_name(dep)] = StartType("permanent") return deps def _app_name(app: Dependency) -> str: """Helper to unwrap the name for an erlang application dependency""" return app[ErlangAppInfo].name

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server