1 # Copyright (c) Meta Platforms, Inc. and affiliates.
3 # This source code is licensed under the MIT license found in the
4 # LICENSE file in the root directory of this source tree.
9 from typing
import List
11 from .builder
import (
14 CMakeBootStrapBuilder
,
24 from .cargo
import CargoBuilder
25 from .expr
import parse_expr
26 from .fetcher
import (
29 PreinstalledNopFetcher
,
30 ShipitTransformerFetcher
,
31 SimpleShipitTransformerFetcher
,
34 from .py_wheel_builder
import PythonWheelBuilder
42 "optional_section": False,
45 "fbsource_path": OPTIONAL
,
46 "shipit_project": OPTIONAL
,
47 "shipit_fbcode_builder": OPTIONAL
,
50 "dependencies": {"optional_section": True, "allow_values": False},
51 "depends.environment": {"optional_section": True},
53 "optional_section": True,
54 "fields": {"repo_url": REQUIRED
, "rev": OPTIONAL
, "depth": OPTIONAL
},
57 "optional_section": True,
58 "fields": {"url": REQUIRED
, "sha256": REQUIRED
},
61 "optional_section": True,
65 "make_binary": OPTIONAL
,
66 "build_in_src_dir": OPTIONAL
,
67 "job_weight_mib": OPTIONAL
,
68 "patchfile": OPTIONAL
,
69 "patchfile_opts": OPTIONAL
,
72 "msbuild": {"optional_section": True, "fields": {"project": REQUIRED
}},
74 "optional_section": True,
76 "build_doc": OPTIONAL
,
77 "workspace_dir": OPTIONAL
,
78 "manifests_to_build": OPTIONAL
,
79 # Where to write cargo config (defaults to build_dir/.cargo/config)
80 "cargo_config_file": OPTIONAL
,
84 "optional_section": True,
86 "run_tests": OPTIONAL
,
89 "crate.pathmap": {"optional_section": True},
90 "cmake.defines": {"optional_section": True},
91 "autoconf.args": {"optional_section": True},
92 "autoconf.envcmd.LDFLAGS": {"optional_section": True},
93 "rpms": {"optional_section": True},
94 "debs": {"optional_section": True},
95 "homebrew": {"optional_section": True},
96 "preinstalled.env": {"optional_section": True},
97 "bootstrap.args": {"optional_section": True},
98 "b2.args": {"optional_section": True},
99 "make.build_args": {"optional_section": True},
100 "make.install_args": {"optional_section": True},
101 "make.test_args": {"optional_section": True},
102 "header-only": {"optional_section": True, "fields": {"includedir": REQUIRED
}},
103 "shipit.pathmap": {"optional_section": True},
104 "shipit.strip": {"optional_section": True},
105 "install.files": {"optional_section": True},
107 "sandcastle": {"optional_section": True, "fields": {"run_tests": OPTIONAL
}},
110 # These sections are allowed to vary for different platforms
111 # using the expression syntax to enable/disable sections
112 ALLOWED_EXPR_SECTIONS
= [
114 "autoconf.envcmd.LDFLAGS",
134 def parse_conditional_section_name(name
, section_def
):
135 expr
= name
[len(section_def
) + 1 :]
136 return parse_expr(expr
, ManifestContext
.ALLOWED_VARIABLES
)
139 def validate_allowed_fields(file_name
, section
, config
, allowed_fields
):
140 for field
in config
.options(section
):
141 if not allowed_fields
.get(field
):
143 ("manifest file %s section '%s' contains " "unknown field '%s'")
144 % (file_name
, section
, field
)
147 for field
in allowed_fields
:
148 if allowed_fields
[field
] == REQUIRED
and not config
.has_option(section
, field
):
150 ("manifest file %s section '%s' is missing " "required field '%s'")
151 % (file_name
, section
, field
)
155 def validate_allow_values(file_name
, section
, config
):
156 for field
in config
.options(section
):
157 value
= config
.get(section
, field
)
158 if value
is not None:
161 "manifest file %s section '%s' has '%s = %s' but "
162 "this section doesn't allow specifying values "
165 % (file_name
, section
, field
, value
)
169 def validate_section(file_name
, section
, config
):
170 section_def
= SCHEMA
.get(section
)
172 for name
in ALLOWED_EXPR_SECTIONS
:
173 if section
.startswith(name
+ "."):
174 # Verify that the conditional parses, but discard it
176 parse_conditional_section_name(section
, name
)
177 except Exception as exc
:
179 ("manifest file %s section '%s' has invalid " "conditional: %s")
180 % (file_name
, section
, str(exc
))
182 section_def
= SCHEMA
.get(name
)
183 canonical_section_name
= name
187 "manifest file %s contains unknown section '%s'" % (file_name
, section
)
190 canonical_section_name
= section
192 allowed_fields
= section_def
.get("fields")
194 validate_allowed_fields(file_name
, section
, config
, allowed_fields
)
195 elif not section_def
.get("allow_values", True):
196 validate_allow_values(file_name
, section
, config
)
197 return canonical_section_name
200 class ManifestParser(object):
201 def __init__(self
, file_name
, fp
=None):
202 # allow_no_value enables listing parameters in the
203 # autoconf.args section one per line
204 config
= configparser
.RawConfigParser(allow_no_value
=True)
205 config
.optionxform
= str # make it case sensitive
208 with
open(file_name
, "r") as fp
:
210 elif isinstance(fp
, type("")):
211 # For testing purposes, parse from a string (str
213 config
.read_file(io
.StringIO(fp
))
217 # validate against the schema
218 seen_sections
= set()
220 for section
in config
.sections():
221 seen_sections
.add(validate_section(file_name
, section
, config
))
223 for section
in SCHEMA
.keys():
224 section_def
= SCHEMA
[section
]
226 not section_def
.get("optional_section", False)
227 and section
not in seen_sections
230 "manifest file %s is missing required section %s"
231 % (file_name
, section
)
234 self
._config
= config
235 self
.name
= config
.get("manifest", "name")
236 self
.fbsource_path
= self
.get("manifest", "fbsource_path")
237 self
.shipit_project
= self
.get("manifest", "shipit_project")
238 self
.shipit_fbcode_builder
= self
.get("manifest", "shipit_fbcode_builder")
239 self
.resolved_system_packages
= {}
241 if self
.name
!= os
.path
.basename(file_name
):
243 "filename of the manifest '%s' does not match the manifest name '%s'"
244 % (file_name
, self
.name
)
247 def get(self
, section
, key
, defval
=None, ctx
=None):
250 for s
in self
._config
.sections():
252 if self
._config
.has_option(s
, key
):
253 return self
._config
.get(s
, key
)
256 if s
.startswith(section
+ "."):
257 expr
= parse_conditional_section_name(s
, section
)
258 if not expr
.eval(ctx
):
261 if self
._config
.has_option(s
, key
):
262 return self
._config
.get(s
, key
)
266 def get_dependencies(self
, ctx
):
267 dep_list
= list(self
.get_section_as_dict("dependencies", ctx
).keys())
269 builder
= self
.get("build", "builder", ctx
=ctx
)
270 if builder
in ("cmake", "python-wheel"):
271 dep_list
.insert(0, "cmake")
272 elif builder
== "autoconf" and self
.name
not in (
277 # they need libtool and its deps (automake, autoconf) so add
278 # those as deps (but obviously not if we're building those
279 # projects themselves)
280 dep_list
.insert(0, "libtool")
284 def get_section_as_args(self
, section
, ctx
=None) -> List
[str]:
285 """Intended for use with the make.[build_args/install_args] and
286 autoconf.args sections, this method collects the entries and returns an
288 If the manifest contains conditional sections, ctx is used to
289 evaluate the condition and merge in the values.
294 for s
in self
._config
.sections():
296 if not s
.startswith(section
+ "."):
298 expr
= parse_conditional_section_name(s
, section
)
299 if not expr
.eval(ctx
):
301 for field
in self
._config
.options(s
):
302 value
= self
._config
.get(s
, field
)
306 args
.append("%s=%s" % (field
, value
))
309 def get_section_as_ordered_pairs(self
, section
, ctx
=None):
310 """Used for eg: shipit.pathmap which has strong
311 ordering requirements"""
315 for s
in self
._config
.sections():
317 if not s
.startswith(section
+ "."):
319 expr
= parse_conditional_section_name(s
, section
)
320 if not expr
.eval(ctx
):
323 for key
in self
._config
.options(s
):
324 value
= self
._config
.get(s
, key
)
325 res
.append((key
, value
))
328 def get_section_as_dict(self
, section
, ctx
):
331 for s
in self
._config
.sections():
333 if not s
.startswith(section
+ "."):
335 expr
= parse_conditional_section_name(s
, section
)
336 if not expr
.eval(ctx
):
338 for field
in self
._config
.options(s
):
339 value
= self
._config
.get(s
, field
)
343 def update_hash(self
, hasher
, ctx
):
344 """Compute a hash over the configuration for the given
345 context. The goal is for the hash to change if the config
346 for that context changes, but not if a change is made to
347 the config only for a different platform than that expressed
348 by ctx. The hash is intended to be used to help invalidate
349 a future cache for the third party build products.
350 The hasher argument is a hash object returned from hashlib."""
351 for section
in sorted(SCHEMA
.keys()):
352 hasher
.update(section
.encode("utf-8"))
354 # Note: at the time of writing, nothing in the implementation
355 # relies on keys in any config section being ordered.
356 # In theory we could have conflicting flags in different
357 # config sections and later flags override earlier flags.
358 # For the purposes of computing a hash we're not super
359 # concerned about this: manifest changes should be rare
360 # enough and we'd rather that this trigger an invalidation
361 # than strive for a cache hit at this time.
362 pairs
= self
.get_section_as_ordered_pairs(section
, ctx
)
363 pairs
.sort(key
=lambda pair
: pair
[0])
364 for key
, value
in pairs
:
365 hasher
.update(key
.encode("utf-8"))
366 if value
is not None:
367 hasher
.update(value
.encode("utf-8"))
369 def is_first_party_project(self
):
370 """returns true if this is an FB first-party project"""
371 return self
.shipit_project
is not None
373 def get_required_system_packages(self
, ctx
):
374 """Returns dictionary of packager system -> list of packages"""
376 "rpm": self
.get_section_as_args("rpms", ctx
),
377 "deb": self
.get_section_as_args("debs", ctx
),
378 "homebrew": self
.get_section_as_args("homebrew", ctx
),
381 def _is_satisfied_by_preinstalled_environment(self
, ctx
):
382 envs
= self
.get_section_as_args("preinstalled.env", ctx
)
386 val
= os
.environ
.get(key
, None)
387 print(f
"Testing ENV[{key}]: {repr(val)}")
395 def get_repo_url(self
, ctx
):
396 return self
.get("git", "repo_url", ctx
=ctx
)
398 def create_fetcher(self
, build_options
, ctx
):
400 ShipitTransformerFetcher
.available() and build_options
.use_shipit
404 and self
.fbsource_path
405 and build_options
.fbsource_dir
406 and self
.shipit_project
408 return SimpleShipitTransformerFetcher(build_options
, self
, ctx
)
412 and build_options
.fbsource_dir
413 and self
.shipit_project
414 and ShipitTransformerFetcher
.available()
416 # We can use the code from fbsource
417 return ShipitTransformerFetcher(build_options
, self
.shipit_project
)
419 # Can we satisfy this dep with system packages?
420 if build_options
.allow_system_packages
:
421 if self
._is
_satisfied
_by
_preinstalled
_environment
(ctx
):
422 return PreinstalledNopFetcher()
424 packages
= self
.get_required_system_packages(ctx
)
425 package_fetcher
= SystemPackageFetcher(build_options
, packages
)
426 if package_fetcher
.packages_are_installed():
427 return package_fetcher
429 repo_url
= self
.get_repo_url(ctx
)
431 rev
= self
.get("git", "rev")
432 depth
= self
.get("git", "depth")
433 return GitFetcher(build_options
, self
, repo_url
, rev
, depth
)
435 url
= self
.get("download", "url", ctx
=ctx
)
437 # We need to defer this import until now to avoid triggering
438 # a cycle when the facebook/__init__.py is loaded.
440 from .facebook
.lfs
import LFSCachingArchiveFetcher
442 return LFSCachingArchiveFetcher(
443 build_options
, self
, url
, self
.get("download", "sha256", ctx
=ctx
)
446 # This FB internal module isn't shippped to github,
447 # so just use its base class
448 return ArchiveFetcher(
449 build_options
, self
, url
, self
.get("download", "sha256", ctx
=ctx
)
453 "project %s has no fetcher configuration matching %s" % (self
.name
, ctx
)
456 def get_builder_name(self
, ctx
):
457 builder
= self
.get("build", "builder", ctx
=ctx
)
459 raise Exception("project %s has no builder for %r" % (self
.name
, ctx
))
462 def create_builder( # noqa:C901
470 final_install_prefix
=None,
471 extra_cmake_defines
=None,
474 builder
= self
.get_builder_name(ctx
)
475 build_in_src_dir
= self
.get("build", "build_in_src_dir", "false", ctx
=ctx
)
476 if build_in_src_dir
== "true":
477 # Some scripts don't work when they are configured and build in
478 # a different directory than source (or when the build directory
479 # is not a subdir of source).
481 subdir
= self
.get("build", "subdir", None, ctx
=ctx
)
482 if subdir
is not None:
483 build_dir
= os
.path
.join(build_dir
, subdir
)
484 print("build_dir is %s" % build_dir
) # just to quiet lint
486 if builder
== "make" or builder
== "cmakebootstrap":
487 build_args
= self
.get_section_as_args("make.build_args", ctx
)
488 install_args
= self
.get_section_as_args("make.install_args", ctx
)
489 test_args
= self
.get_section_as_args("make.test_args", ctx
)
490 if builder
== "cmakebootstrap":
491 return CMakeBootStrapBuilder(
515 if builder
== "autoconf":
516 args
= self
.get_section_as_args("autoconf.args", ctx
)
518 ldflags_cmd
= self
.get_section_as_args("autoconf.envcmd.LDFLAGS", ctx
)
520 conf_env_args
["LDFLAGS"] = ldflags_cmd
521 return AutoconfBuilder(
532 if builder
== "boost":
533 args
= self
.get_section_as_args("b2.args", ctx
)
534 if extra_b2_args
is not None:
535 args
+= extra_b2_args
536 return Boost(build_options
, ctx
, self
, src_dir
, build_dir
, inst_dir
, args
)
538 if builder
== "cmake":
539 defines
= self
.get_section_as_dict("cmake.defines", ctx
)
549 final_install_prefix
,
553 if builder
== "python-wheel":
554 return PythonWheelBuilder(
555 build_options
, ctx
, self
, src_dir
, build_dir
, inst_dir
558 if builder
== "sqlite":
559 return SqliteBuilder(build_options
, ctx
, self
, src_dir
, build_dir
, inst_dir
)
561 if builder
== "ninja_bootstrap":
562 return NinjaBootstrap(
563 build_options
, ctx
, self
, build_dir
, src_dir
, inst_dir
567 return NopBuilder(build_options
, ctx
, self
, src_dir
, inst_dir
)
569 if builder
== "openssl":
570 return OpenSSLBuilder(
571 build_options
, ctx
, self
, build_dir
, src_dir
, inst_dir
574 if builder
== "iproute2":
575 return Iproute2Builder(
576 build_options
, ctx
, self
, src_dir
, build_dir
, inst_dir
579 if builder
== "cargo":
580 return self
.create_cargo_builder(
581 build_options
, ctx
, src_dir
, build_dir
, inst_dir
, loader
584 if builder
== "OpenNSA":
585 return OpenNSABuilder(build_options
, ctx
, self
, src_dir
, inst_dir
)
587 raise KeyError("project %s has no known builder" % (self
.name
))
589 def create_prepare_builders(
590 self
, build_options
, ctx
, src_dir
, build_dir
, inst_dir
, loader
592 """Create builders that have a prepare step run, e.g. to write config files"""
593 prepare_builders
= []
594 builder
= self
.get_builder_name(ctx
)
595 cargo
= self
.get_section_as_dict("cargo", ctx
)
596 if not builder
== "cargo" and cargo
:
597 cargo_builder
= self
.create_cargo_builder(
598 build_options
, ctx
, src_dir
, build_dir
, inst_dir
, loader
600 prepare_builders
.append(cargo_builder
)
601 return prepare_builders
603 def create_cargo_builder(
604 self
, build_options
, ctx
, src_dir
, build_dir
, inst_dir
, loader
606 build_doc
= self
.get("cargo", "build_doc", False, ctx
)
607 workspace_dir
= self
.get("cargo", "workspace_dir", None, ctx
)
608 manifests_to_build
= self
.get("cargo", "manifests_to_build", None, ctx
)
609 cargo_config_file
= self
.get("cargo", "cargo_config_file", None, ctx
)
625 class ManifestContext(object):
626 """ProjectContext contains a dictionary of values to use when evaluating boolean
627 expressions in a project manifest.
629 This object should be passed as the `ctx` parameter in ManifestParser.get() calls.
632 ALLOWED_VARIABLES
= {
642 def __init__(self
, ctx_dict
):
643 assert set(ctx_dict
.keys()) == self
.ALLOWED_VARIABLES
644 self
.ctx_dict
= ctx_dict
647 return self
.ctx_dict
[key
]
649 def set(self
, key
, value
):
650 assert key
in self
.ALLOWED_VARIABLES
651 self
.ctx_dict
[key
] = value
654 return ManifestContext(dict(self
.ctx_dict
))
658 "%s=%s" % (key
, value
) for key
, value
in sorted(self
.ctx_dict
.items())
663 class ContextGenerator(object):
664 """ContextGenerator allows creating ManifestContext objects on a per-project basis.
665 This allows us to evaluate different projects with slightly different contexts.
667 For instance, this can be used to only enable tests for some projects."""
669 def __init__(self
, default_ctx
):
670 self
.default_ctx
= ManifestContext(default_ctx
)
671 self
.ctx_by_project
= {}
673 def set_value_for_project(self
, project_name
, key
, value
):
674 project_ctx
= self
.ctx_by_project
.get(project_name
)
675 if project_ctx
is None:
676 project_ctx
= self
.default_ctx
.copy()
677 self
.ctx_by_project
[project_name
] = project_ctx
678 project_ctx
.set(key
, value
)
680 def set_value_for_all_projects(self
, key
, value
):
681 self
.default_ctx
.set(key
, value
)
682 for ctx
in self
.ctx_by_project
.values():
685 def get_context(self
, project_name
):
686 return self
.ctx_by_project
.get(project_name
, self
.default_ctx
)