Gestione delle dipendenze esterne con Bzlmod

Bzlmod è il nome in codice del nuovo sistema di dipendenza esterna introdotto in Bazel 5.0. È stato introdotto per affrontare diversi punti critici del vecchio sistema che non potevano essere risolti in modo incrementale; Consulta la sezione relativa alla definizione del problema nel documento di progettazione originale per maggiori dettagli.

In Bazel 5.0, Bzlmod non è attivo per impostazione predefinita; Per rendere effettivo il flag --experimental_enable_bzlmod, è necessario specificare il flag seguente. Come suggerisce il nome della segnalazione, questa funzionalità è attualmente sperimentale; API e comportamenti possono cambiare fino al lancio ufficiale della funzionalità.

Moduli Bazel

Il vecchio sistema di dipendenza esterno basato su WORKSPACE è centrato sui repository (o repos), creati tramite le regole di repository (o regole repository). Mentre i repository sono ancora un concetto importante nel nuovo sistema, i moduli sono le unità principali di dipendenza.

Un modulo è essenzialmente un progetto Bazel che può avere più versioni, ognuna delle quali pubblica metadati su altri moduli da cui dipende. Si tratta di un concetto analogo a quello di altri sistemi di gestione delle dipendenze: un artefatto Maven, un pacchetto npm, una crate Cargo , un modulo Go e così via.

Un modulo specifica semplicemente le sue dipendenze utilizzando coppie name e version, anziché URL specifici in WORKSPACE. Le dipendenze vengono quindi cercate in un registro Baizel; per impostazione predefinita, Bazel Central Registry. Nell'area di lavoro, ogni modulo diventa quindi un repository.

MODULE.bazel

Ogni versione di ogni modulo ha un file MODULE.bazel che ne dichiara le dipendenze e altri metadati. Ecco un esempio di base:

module(
    name = "my-module",
    version = "1.0",
)

bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")

Il file MODULE.bazel deve trovarsi nella directory principale della directory dell'area di lavoro (accanto al file WORKSPACE). A differenza del file WORKSPACE, non è necessario specificare le dipendenze transitive; Devi invece specificare solo le dipendenze dirette e i file MODULE.bazel delle tue dipendenze verranno elaborati per rilevare automaticamente le dipendenze transitorie.

Il file MODULE.bazel è simile ai file BUILD perché non supporta alcuna forma di flusso di controllo; vieta inoltre le istruzioni load. Le istruzioni MODULE.bazel supportate dai file sono:

Formato della versione

Bazel ha un ecosistema diversificato e i progetti utilizzano diversi schemi di controllo delle versioni. Il più popolare è in assoluto SemVer, ma esistono anche progetti di spicco che utilizzano schemi diversi come Abseil, le cui versioni sono basato sulla data, ad esempio 20210324.2).

Per questo motivo, Bzlmod adotta una versione più flessibile della specifica SemVer, in particolare consentendo un numero qualsiasi di sequenze di cifre nella parte "release" della versione (anziché esattamente tre come prescritto da SemVer: MAJOR.MINOR.PATCH). Inoltre, la semantica degli aumenti principali, minori e delle patch non viene applicata. Tuttavia, consulta il livello di compatibilità per maggiori dettagli su come intendiamo la compatibilità con le versioni precedenti. Altre parti delle specifiche SemVer, ad esempio un trattino che indica una versione non definitiva, non vengono modificate.

Risoluzione delle versioni

Il problema della dipendenza del rombo è un elemento fondamentale nello spazio di gestione delle dipendenze con versione. Supponi di avere il seguente grafico di dipendenza:

       A 1.0
      /     \
   B 1.0    C 1.1
     |        |
   D 1.0    D 1.1

Quale versione di D deve essere utilizzata? Per risolvere questo problema, Bzlmod utilizza l'algoritmo Scelta versione minima (MVS) introdotta nel sistema del modulo Go. MVS presuppone che tutte le nuove versioni di un modulo siano compatibili con le versioni precedenti e quindi sceglie semplicemente la versione più alta specificata da qualsiasi dipendenza (D 1.1 nel nostro esempio). Si chiama "minimal" perché D 1.1 ecco la versione minimal in grado di soddisfare i nostri requisiti. Anche se esiste D 1.2 o successivo, non li selezioniamo. Questo offre il vantaggio aggiuntivo che la selezione della versione è alta fedeltà e riproducibile.

La risoluzione delle versioni viene eseguita localmente sul computer, non dal registro.

Livello di compatibilità

Tieni presente che l'ipotesi di MVS sulla compatibilità con le versioni precedenti è fattibile, perché considera semplicemente le versioni non compatibili con le versioni precedenti di un modulo come un modulo separato. In termini di SemVer, ciò significa che A 1.x e A 2.x sono considerati moduli distinti e possono coesistere nel grafico delle dipendenze risolto. Ciò è possibile a sua volta dal fatto che la versione principale è codificata nel percorso del pacchetto in Go, quindi non si verificano conflitti in fase di compilazione o in fase di collegamento.

Per Bazel non disponiamo di garanzie. Abbiamo quindi bisogno di un modo per indicare il numero di versione principale per rilevare le versioni incompatibili con le versioni precedenti. Questo numero è denominato livello di compatibilità ed è specificato da ogni versione del modulo nella relativa istruzione module(). Con queste informazioni a portata di mano, possiamo generare un errore quando rileviamo che nel grafico delle dipendenze risolto esistono versioni dello stesso modulo con livelli di compatibilità diversi.

Nomi repository

In Bazel, ogni dipendenza esterna ha un nome di repository. A volte, la stessa dipendenza può essere utilizzata tramite nomi di repository diversi (ad esempio, @io_bazel_skylib e @bazel_skylib indicano Bazel skylib) o la stessa Il nome del repository potrebbe essere utilizzato per diverse dipendenze in progetti diversi.

In Bzlmod, i repository possono essere generati dai moduli Bazel e dalle estensioni modulo. Per risolvere i conflitti relativi ai nomi dei repository, stiamo adottando il meccanismo di mappatura dei repository nel nuovo sistema. Di seguito sono riportati due aspetti importanti da ricordare.

  • Nome repository canonico: il nome univoco del repository a livello globale per ogni repository. Questo sarà il nome di directory in cui si trova il repository.
    Il modello è strutturato come segue (Avviso: il formato del nome canonico non è un'API da cui dipende, è soggetto a modifiche in qualsiasi momento):

    • Per i repository modulo Bazel: module_name.version
      (esempio. @bazel_skylib.1.0.3)
    • Per i repository delle estensioni modulo: module_name.version.extension_name.repo_name
      (Esempio. @rules_cc.0.0.1.cc_configure.local_config_cc)
  • Nome repository locale: il nome del repository da utilizzare nei file BUILD e .bzl all'interno di un repository. La stessa dipendenza potrebbe avere nomi locali diversi per repository diversi.
    È determinato come segue:

    • Per i repository modulo Bazel: module_name per impostazione predefinita o il nome specificato dall'attributo repo_name in bazel_dep.
    • Per i repository estensione di modulo: nome del repository introdotto tramite use_repo.

Ogni repository ha un dizionario di mappatura dei repository come dipendenze dirette, che è una mappa dal nome del repository locale al nome del repository canonico. Utilizziamo la mappatura del repository per risolvere il nome del repository durante la creazione di un'etichetta. Tieni presente che non vi è alcun conflitto di nomi di repository canonici e l'utilizzo dei nomi di repository locali può essere rilevato mediante l'analisi del file MODULE.bazel. Di conseguenza, i conflitti possono essere facilmente rilevati e risolti senza influire sui dati. altre dipendenze.

Scarte rigide

Il nuovo formato della specifica delle dipendenze ci consente di eseguire controlli più rigidi. In particolare, ora applichiamo che un modulo può utilizzare solo i repository creati dalle sue dipendenze dirette. In questo modo si evitano interruzioni accidentali e difficili da eseguire il debug quando cambia qualcosa nel grafico delle dipendenze transitorie.

I deployment di livello massimo sono implementati in base alla mappatura del repository. In pratica, la mappatura del repository per ogni repository contiene tutte le sue dipendenze dirette. Qualsiasi altro repository non è visibile. Le dipendenze visibili per ogni repository sono determinate come segue:

  • Un repository modulo Bazel può vedere tutti i repository introdotti nel file MODULE.bazel tramite bazel_dep e use_repo.
  • Un repository di estensioni modulo può vedere tutte le dipendenze visibili del modulo che fornisce l'estensione, più tutti gli altri repository generati dalla stessa estensione modulo.

Registri

Bzlmod rileva le dipendenze richiedendo le informazioni dai registri di Bazel. Un registro Bazel è semplicemente un database di moduli Bazel. L'unica forma supportata dei registry è un registro degli indici, ovvero una directory locale o un server HTTP statico che segue un formato specifico. In futuro, prevediamo di aggiungere il supporto per i registri di moduli singoli, che sono semplicemente repository Git che contengono l'origine e la cronologia di un progetto.

Registro di indice

Un registro di indice è una directory locale o un server HTTP statico contenente informazioni su un elenco di moduli, inclusi home page, manutentori, file MODULE.bazel di ogni versione e informazioni su come recuperare l'origine di ogni version. In particolare, non è necessario pubblicare gli archivi di origine.

Un registry di indice deve avere il seguente formato:

  • /bazel_registry.json: un file JSON contenente metadati per il Registro di sistema. Attualmente, ha una sola chiave, mirrors, che specifica l'elenco di mirror da utilizzare per gli archivi di origine.
  • /modules: una directory contenente una sottodirectory per ogni modulo di questo registro.
  • /modules/$MODULE: una directory contenente una sottodirectory per ogni versione di questo modulo, nonché il seguente file:
    • metadata.json: un file JSON contenente informazioni sul modulo, con i seguenti campi:
      • homepage: l'URL della home page del progetto.
      • maintainers: un elenco di oggetti JSON, ognuno dei quali corrisponde alle informazioni di un gestore del modulo nel registro. Tieni presente che non si tratta necessariamente degli author del progetto.
      • versions: un elenco di tutte le versioni di questo modulo presenti nel registro.
      • yanked_versions: un elenco delle versioni yank di questo modulo. Al momento si tratta di un'operazione autonoma, ma in futuro le versioni con yak verranno ignorate e generano un errore.
  • /modules/$MODULE/$VERSION: una directory contenente i seguenti file:
    • MODULE.bazel: il file MODULE.bazel di questa versione del modulo.
    • source.json: un file JSON contenente informazioni su come recuperare l'origine di questa versione del modulo, con i seguenti campi:
      • url: l'URL dell'archivio di origine.
      • integrity: checksum Integrità risorsa secondaria dell'archivio.
      • strip_prefix: un prefisso della directory da rimuovere durante l'estrazione dell'archivio di origine.
      • patches: un elenco di stringhe, ciascuna delle quali assegna un nome a un file di patch da applicare all'archivio estratto. I file patch si trovano nella directory /modules/$MODULE/$VERSION/patches.
      • patch_strip: uguale all'argomento --strip della patch Unix.
    • patches/: una directory facoltativa contenente file patch.

Registro centrale Bazel

Bazel Central Registry (BCR) è un registro di indice situato all'indirizzo registry.bazel.build. I suoi contenuti sono supportati dal repository GitHub bazelbuild/bazel-central-registry.

Il BCR è gestito dalla community di Bazel; i collaboratori sono invitati a inviare richieste di pull. Consulta Criteri e procedure del Registro di sistema centrale di Bazel.

Oltre a seguire il formato di un normale registro degli indici, il BCR richiede un file presubmit.yml per ogni versione del modulo (/modules/$MODULE/$VERSION/presubmit.yml). Questo file specifica alcune destinazioni essenziali di build e test che possono essere utilizzate per verificare la validità di questa versione del modulo e vengono utilizzate dalle pipeline CI di BCR per garantire l'interoperabilità tra i moduli nella BCR ,

Selezione dei registri

Il flag Bazel ripetibile --registry può essere utilizzato per specificare l'elenco di registry da cui richiedere i moduli, in modo da poter configurare il progetto per recuperare le dipendenze da un registro di terze parti o interno. I registri precedenti hanno la precedenza. Per praticità, puoi inserire un elenco di flag --registry nel file .bazelrc del tuo progetto.

Estensioni modulo

Le estensioni modulo consentono di estendere il sistema dei moduli leggendo i dati di input dai moduli nel grafico delle dipendenze, eseguendo la logica necessaria per risolvere le dipendenze e infine creando repository utilizzando le regole repository. Sono simili alle funzioni delle macro WORKSPACE di oggi, ma sono più adatte nel mondo dei moduli e delle dipendenze transitorie.

Le estensioni modulo vengono definite nei file .bzl, proprio come le regole repository o le macro WORKSPACE. Non vengono richiamati direttamente. Ogni modulo può invece specificare dati detti tag da far leggere alle estensioni. Successivamente, al termine della risoluzione della versione del modulo, le estensioni modulo vengono eseguite. Ogni estensione viene eseguita dopo la risoluzione del modulo (ancora prima che venga creata una build) e leggi tutti i tag che appartengono all'intera tabella di dipendenza.

          [ A 1.1                ]
          [   * maven.dep(X 2.1) ]
          [   * maven.pom(...)   ]
              /              \
   bazel_dep /                \ bazel_dep
            /                  \
[ B 1.2                ]     [ C 1.0                ]
[   * maven.dep(X 1.2) ]     [   * maven.dep(X 2.1) ]
[   * maven.dep(Y 1.3) ]     [   * cargo.dep(P 1.1) ]
            \                  /
   bazel_dep \                / bazel_dep
              \              /
          [ D 1.4                ]
          [   * maven.dep(Z 1.4) ]
          [   * cargo.dep(Q 1.1) ]

Nel grafico delle dipendenze di esempio riportato sopra, A 1.1, B 1.2 e così via sono moduli Bazel; puoi considerare ciascuno di essi come un file MODULE.bazel. Ogni modulo può specificare alcuni tag per le estensioni modulo. qui vengono specificati per l'estensione "maven" e altri per "cargo". Quando il grafico di dipendenza è finalizzato (ad esempio, B 1.2 in realtà ha un bazel_dep su D 1.3 ma è stato eseguito l'upgrade a D 1.4 a causa di C), viene eseguita ed è in grado di leggere tutti i tag maven.*, utilizzando le informazioni al suo interno per decidere quali repository creare. Analogamente all'estensione "cargo".

Utilizzo delle estensioni

Le estensioni sono ospitate nei moduli Bazel stessi, pertanto per utilizzare un'estensione nel modulo, devi prima aggiungere un elemento bazel_dep su quel modulo, quindi richiamare use_extension per integrarlo nell'ambito di applicazione. Prendiamo come esempio il seguente esempio, uno snippet da un file MODULE.bazel per utilizzare un'ipotetica estensione "maven" definita nel modulo rules_jvm_external:

bazel_dep(name = "rules_jvm_external", version = "1.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

Dopo aver portato l'estensione nell'ambito, puoi utilizzare la sintassi punto per specificare i tag. Tieni presente che i tag devono seguire lo schema definito dalle classi di tag corrispondenti (consulta la definizione delle estensioni di seguito). Di seguito è riportato un esempio che specifica alcuni tag maven.dep e maven.pom.

maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")

Se l'estensione genera repository che vuoi utilizzare nel modulo, utilizza l'istruzione use_repo per dichiararli. In questo modo si soddisfano le rigide condizioni dei deep ed evitano conflitti tra i nomi dei repository locali.

use_repo(
    maven,
    "org_junit_junit",
    guava="com_google_guava_guava",
)

I repository generati da un'estensione fanno parte della sua API, quindi dai tag che hai specificato, dovresti sapere che l'estensione "maven" genererà un repository denominato "org_junit_junit" e un file chiamato "com_google_guava_guava ". Con use_repo, se vuoi, puoi rinominarle nell'ambito del modulo, ad esempio in "guava" qui.

Definizione dell'estensione

Le estensioni modulo vengono definite in modo simile alle regole repository, utilizzando la funzione module_extension. Entrambi hanno una funzione di implementazione; Tuttavia, anche se le regole del repository hanno vari attributi, le estensioni modulo hanno un numero di tag_classes, ognuno dei quali ha un numero di attributi. Le classi di tag definiscono gli schemi per i tag utilizzati da questa estensione. Per continuare con il nostro esempio dell'ipotetica estensione "maven" descritta in precedenza:

# @rules_jvm_external//:extensions.bzl
maven_dep = tag_class(attrs = {"coord": attr.string()})
maven_pom = tag_class(attrs = {"pom_xml": attr.label()})
maven = module_extension(
    implementation=_maven_impl,
    tag_classes={"dep": maven_dep, "pom": maven_pom},
)

Queste dichiarazioni chiariscono che i tag maven.dep e maven.pom possono essere specificati utilizzando lo schema dell'attributo descritto in precedenza.

La funzione di implementazione è simile a una macro WORKSPACE, con la differenza che ha un oggetto module_ctx che concede l'accesso al grafico delle dipendenze e a tutti i tag pertinenti. La funzione di implementazione deve quindi chiamare le regole repository per generare repository:

# @rules_jvm_external//:extensions.bzl
load("//:repo_rules.bzl", "maven_single_jar")
def _maven_impl(ctx):
  coords = []
  for mod in ctx.modules:
    coords += [dep.coord for dep in mod.tags.dep]
  output = ctx.execute(["coursier", "resolve", coords])  # hypothetical call
  repo_attrs = process_coursier(output)
  [maven_single_jar(**attrs) for attrs in repo_attrs]

Nell'esempio sopra, analizzeremo tutti i moduli del grafico delle dipendenze (ctx.modules), ognuno dei quali è un oggetto bazel_module il cui tags mostra tutti i tag maven.* nel modulo. In seguito, richiamo l'utilità CLI Coursier per contattare Maven ed eseguire la risoluzione. Infine, utilizziamo la risoluzione per creare un numero di repository utilizzando l'ipotetica regola del repository maven_single_jar.