{"id":15021074,"url":"https://github.com/morfly/airin","last_synced_at":"2025-10-27T10:31:52.281Z","repository":{"id":41408819,"uuid":"368910388","full_name":"Morfly/airin","owner":"Morfly","description":"Airin is a tool for the automated migration of Gradle Android projects to Bazel","archived":false,"fork":false,"pushed_at":"2024-05-12T00:41:53.000Z","size":59393,"stargazers_count":37,"open_issues_count":2,"forks_count":5,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-10-11T11:21:59.509Z","etag":null,"topics":["android","bazel","build","gradle","java","kotlin","starlark"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Morfly.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-05-19T15:08:28.000Z","updated_at":"2024-07-22T20:21:12.000Z","dependencies_parsed_at":"2023-12-18T22:52:41.270Z","dependency_job_id":"9ae1a865-efc3-4931-bdbc-6a4d27b1a64b","html_url":"https://github.com/Morfly/airin","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Morfly%2Fairin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Morfly%2Fairin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Morfly%2Fairin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Morfly%2Fairin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Morfly","download_url":"https://codeload.github.com/Morfly/airin/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":219861468,"owners_count":16555994,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","bazel","build","gradle","java","kotlin","starlark"],"created_at":"2024-09-24T19:56:06.239Z","updated_at":"2025-10-27T10:31:51.526Z","avatar_url":"https://github.com/Morfly.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Airin 🎋\nAirin is a tool for the automated migration of Gradle Android projects to Bazel.\n\n- [Overview](#overview)\n- [Gradle plugin](#gradle-plugin)\n- [Module components](#module-components)\n- [Feature components](#feature-components)\n- [Shared components](#shared-components)\n- [Properties](#properties)\n- [Decorators](#decorators)\n\n## Overview\nTo facilitate the migration of Android apps to Bazel, Airin provides a Gradle plugin that upon configuration, \nanalyzes the Gradle project structure and replicates it with Bazel by generating the corresponding Bazel files.\n\nTo enable Starlark code generation in Kotlin, Airin is bundled with [Pendant](https://github.com/Morfly/pendant), \nan open-source declarative and type-safe Starlark code generator.\n\n\u003e You can learn more about Airin and the design behind it in a [blog post](https://morfly.medium.com/6dc79d298628) at Turo Engineering.\n\n### Installation\nApply Airin Gradle plugin in your root `build.gradle.kts` file.\n```kotlin\n// root build.gradle.kts\nplugins {\n    id(\"io.morfly.airin.android\") version \"x.y.z\"\n}\n```\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.morfly.airin/airin-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.morfly.airin/airin-core)\n### Configuration\nNext, in the same `build.gradle.kts` file, use the `airin` extension to configure the plugin and adjust it specifically \nfor your project.\n\n```kotlin\n// root build.gradle.kts\nairin {\n    targets += setOf(\":app\")\n\n    register\u003cAndroidLibraryModule\u003e {\n        include\u003cJetpackComposeFeature\u003e()\n        include\u003cHiltFeature\u003e()\n        include\u003cParcelizeFeature\u003e()\n        ...\n    }\n    register\u003cJvmLibraryModule\u003e()\n\n    register\u003cRootModule\u003e {\n        include\u003cAndroidToolchainFeature\u003e()\n    }\n}\n```\nA few things are happening in the script above:\n- An`app` module and all its dependencies will be migrated to Bazel.\n- Modules classified as Android library, JVM library and root module will be migrated to Bazel.\n- If any Android library module optionally uses technologies like Jetpack Compose, Hilt or Parcelize, they will be reflected in Bazel too.\n\nContinue reading the documentation to find more details about components and plugin configuration.\n\n### Migration\nFinally, after the plugin is configured, the migration to Bazel is triggered with a corresponding Gradle task.\n```shell\n./gradlew app:migrateToBazel --no-configure-on-demand\n```\n\n\u003e Airin plugin needs to analyze the project dependency graph during the configuration phase. \n\u003e Therefore, [configuration on demand](https://docs.gradle.org/current/userguide/multi_project_configuration_and_execution.html#sec:configuration_on_demand) must be disabled when running the migration. \n\n## Gradle plugin\n\n### Configuration options\nTo configure Airin Gradle plugin use `airin` extension in the root `build.gradle.kts` file.\n\n- `targets` - configure migration targets. A `migrateToBazel` task as assigned to each migration target and triggers the migration for all its dependencies as well as the root module of the project.\n- `skippedProjects` - ignore these Gradle projects during the Bazel migration.\n- `configurations` - specify allowed Gradle dependency configurations during the migration. All the rest dependencies will be ignored in Bazel.\n- `register` - register a [module component](#module-components) that targets a specific type of modules. See [module components](#module-components).\n- `include` - include a [feature component](#feature-components) in a module component that targets specific build features included in the module. Must be applied under the specific module component. See [feature components](#feature-components).\n- `onComponentConflict` - configure the behavior when Airin finds more then one module component that can migrate a module.\n  - `Fail` - fail the build.\n  - `UsePriority` - pick one with higher priority.\n  - `Ignore` - ignore the module.\n- `onMissingComponent` - configure the behavior when Airin can't find any module component to migrate a module.\n  - `Fail` - fail the build.\n  - `Ignore` - ignore the module.\n- `decorateWith` - register a custom module decorator. See [decorators](#decorators).\n\n### Gradle tasks\n- `migrateToBazel` - registered for each migration target that is explicitly specified in `airin` plugin extension. Triggers the migration for the module, its direct and transitive dependencies and a root module.\n- `migrateProjectToBazel` - registered for all dependencies of migration targets. Triggers migration only for this module.\n- `migrateRootToBazelFor***` - registered for a root project and complements migration for specific migration target, where the `***` is a name of a migration target. E.g. `:migrateRootToBazelForApp`.\n\n## Module components\nModule component is responsible for generating Bazel files for specific types of modules.\n\nEvery module component is an abstract class that extends the ModuleComponent base class and implements 2 functions, `canProcess` and `onInvoke`.\n\nIt is declared in `buildSrc`, or any other type of module that is included in the classpath of your build configuration.\n\n```kotlin\nabstract class AndroidLibraryModule : ModuleComponent {\n\n  override fun canProcess(project: Project): Boolean {...}\n\n  override fun ModuleContext.onInvoke(module: GradleModule) {...}\n}\n```\n\n- `canProcess` is invoked during the Gradle Configuration phase and is aimed to filter Gradle modules to which this component is applicable. Only one module component can be selected for every module in the codebase.\n- `onInvoke` is invoked during the Gradle Execution phase and contains the main logic of the component the purpose of which is to generate Bazel files for the module.\n\nThe easiest way to determine the module type is by examining its applied plugins. For example, an Android library module in Gradle typically relies upon the `com.android.library` plugin.\n\n```kotlin\nabstract class AndroidLibraryModule : ModuleComponent {\n\n  override fun canProcess(project: Project): Boolean =\n    project.plugins.hasPlugin(\"com.android.library\")\n}\n```\n### Generating Bazel files\nThe main responsibility of module components is generating Bazel files. \nTo do this, Airin leverages Pendant, a Kotlin DSL that provides a declarative API for generating Starlark code. This is done in `onInvoke`.\n\n```kotlin\nabstract class AndroidLibraryModule : ModuleComponent {\n\n  override fun ModuleContext.onInvoke(module: GradleModule) { \n    val file = BUILD.bazel {\n        ...\n    }\n    generate(file)\n  }\n}\n```\nYou can use various builders for different types of Bazel types.\n```kotlin\nval build = BUILD { ... }\nval buildWithExt = BUILD.bazel { ... }\nval workspace = WORKSPACE { ... }\nval workspaceWithExt = WORKSPACE.bazel { ... }\nval mavenDependencies = \"maven_dependencies\".bzl { ... }\n```\nTo write the file in the file system, use `generate` call. By default, the file is generated in the same directory as the currently processed module. \nAdditionally, you can use `relativePath` to specify a subdirectory for a generated file.\n```kotlin\ngenerate(build)\ngenerate(mavenDependencies, relativePath = \"third_party\")\n```\n\nThe actual content of a generated Starlark file is built using [Pendant](https://github.com/Morfly/pendant).\n\n```kotlin\noverride fun ModuleContext.onInvoke(module: GradleModule) {\n  val file = BUILD.bazel {\n    load(\"@io_bazel_rules_kotlin//kotlin:android.bzl\", \"kt_android_library\")\n  \n    kt_android_library {\n      name = module.name\n      srcs = glob(\"src/main/**/*.kt\")\n      custom_package = module.androidMetadata?.packageName\n      manifest = \"src/main/AndroidManifest.xml\"\n      resource_files = glob(\"src/main/res/**\")\n    }\n  }\n\n  generate(file)\n}\n```\n\n\u003e You can find an in-depth overview of Pendant on [GitHub](https://github.com/Morfly/pendant) and in the talk at [droidcon NYC 2022](https://www.droidcon.com/2022/09/29/advanced-techniques-for-building-kotlin-dsls/).\n\n### Dependencies\nA Bazel target can possess various types of dependencies, each represented by different function parameters.\n\n```python\n# BUILD.bazel\nkt_android_library(\n  ...\n  deps = [...],\n  exports = [...],\n  plugins = [...],\n)\n```\nA `GradleModule` instance provides dependencies mapped per configuration, represented with an argument name. To designate dependencies in the generated code, the `=` function (enclosed in backticks) is used to represent an argument passed to a function.\n\n```python\nkt_android_library {\n  ...\n  for ((config, deps) in module.dependencies) {\n    config `=` deps.map { it.asBazelLabel().toString() }\n  }\n}\n```\nAs a result, the following Starlark code is generated.\n\n```python\n# generated Bazel script\nkt_android_library(\n  ...\n  plugins = [...],\n  deps = [...],\n  exports = [...],\n)\n```\n## Feature components\nFeature component is responsible for contributing to Bazel files generated by a related module component based on a specific build feature.\n\nEvery feature component is an abstract class that extends the `FeatureComponent` base class and implements 2 functions, `canProcess` and `onInvoke`.\n\nIt is declared in `buildSrc`, or any other type of module that is included in the classpath of your build configuration.\n\n```kotlin\nabstract class JetpackComposeFeature : FeatureComponent() {\n\n  override fun canProcess(project: Project): Boolean {...}\n\n  override fun FeatureContext.onInvoke(module: GradleModule) {...}\n}\n```\n`canProcess` is invoked during the Gradle Configuration phase and is aimed to filter Gradle modules to which this component is applicable.\n\n`onInvoke` is invoked during the Gradle Execution phase and contains the main logic of the component. Its purpose is to modify Bazel files generated by a related module component as well as manage the dependencies of the module.\n\n### Dependency overrides\nWhen migrating Gradle modules to Bazel, an obvious task is to preserve a correct dependency graph including internal module dependencies and third-party artifacts. As it turns out, the same module might have a different set of dependencies in Gradle and Bazel.\n\nTo address such scenarios, feature components offer dependency override API.\n\n```kotlin\n// FeatureComponent.onInvoke\nonDependency(MavenCoordinates(\"com.google.dagger\", \"hilt-android\")) {\n  overrideWith(BazelLabel(path = \"\", target = \"hilt-android\"))\n}\n```\nIn addition, you can ignore certain Gradle dependencies in Bazel by leaving the `onDependency` block empty.\n```kotlin\n// FeatureComponent.onInvoke\nonDependency(MavenCoordinates(\"com.google.dagger\", \"hilt-android\")) {\n  // ignored\n}\n```\n\u003e Refer to the example of [`HiltFeature`](https://github.com/Morfly/airin/blob/989df0b03a2a38c4390839104de3b1f248ffcaca/airin-gradle-android/src/main/kotlin/io/morfly/airin/feature/HiltFeature.kt) to learn more.\n\n### Configuration overrides\nWhen setting up a Gradle module, it involves not only specifying dependencies but also assigning them to a specific **configuration**, providing instructions to Gradle on how to treat each dependency.\n```kotlin\n// build.gradle.kts\ndependencies {\n  implementation(...)\n  api(...)\n  ksp(...)\n  ...\n}\n```\nIn Bazel, targets are declared using function calls. As an analogue to Gradle configurations, we use specific function parameters for various types of dependencies.\n```python\n# BUILD.bazel\nkt_android_library(\n  ...\n  deps = [...],\n  exports = [...],\n  plugins = [...],\n)\n```\nSimilar to dependency overrides, feature components also allow the overriding of configurations. In the example below, all `implementation` dependencies will be declared as `deps` in Bazel.\n\n```kotlin\n// FeatureComponent.onInvoke\nonConfiguration(\"implementation\") {\n  overrideWith(\"deps\")\n}\n```\nFor exporting transitive dependencies, `deps` and `exports` are used as an equivalent to Gradle’s `api` configuration. \n```kotlin\n// FeatureComponent.onInvoke\nonConfiguration(\"api\") {\n  overrideWith(\"deps\")\n  overrideWith(\"exports\")\n}\n```\n\n\u003e Refer to the example of [`ArtifactMappingFeature`](https://github.com/Morfly/airin/blob/989df0b03a2a38c4390839104de3b1f248ffcaca/airin-gradle-android/src/main/kotlin/io/morfly/airin/feature/ArtifactMappingFeature.kt) to learn more.\n\n\n### File modifiers\nBeyond handling dependencies, feature components can also make contributions to the Bazel files generated by module components.\n\nLet’s revisit the code snippet from the `AndroidLibraryModule` component that we used in the [module components](#module-components) section, but this time, let’s make a slight update to it.\n```kotlin\n// ModuleComponent.onInvoke\nval file = BUILD.bazel {\n  _id = \"build_file\"\n\n  load(\"@io_bazel_rules_kotlin//kotlin:android.bzl\", \"kt_android_library\")\n\n  kt_android_library {\n    _id = \"android_library_target\"\n    \n    name = module.name\n    srcs = glob(\"src/main/**/*.kt\")\n    custom_package = module.androidMetadata?.packageName\n    manifest = \"src/main/AndroidManifest.xml\"\n    resource_files = glob(\"src/main/res/**\")\n    for ((config, deps) in module.dependencies) {\n      config `=` deps.map { it.asBazelLabel().toString() }\n    }\n  }\n}\n```\n\nA notable addition here is the introduction of `_id` fields. These can be defined within any code block enclosed by curly brackets `{}`. Once defined, you gain the flexibility to edit the contents of these code blocks externally.\n\nLet’s modify the contents of the generated Bazel file using our feature component. To achieve this, within the `onInvoke` function, use the `onContext` call, specifying the type of the context to be modified, along with its identifier.\n\n```kotlin\n// FeatureComponent.onInvoke\nonContext\u003cBuildContext\u003e(id = \"build_file\") {\n  `package`(default_visibility = list[\"//visibility:public\"])\n}\n\n\nonContext\u003cKtAndroidLibraryContext\u003e(id = \"android_library_target\") {\n  enable_data_binding = true\n}\n```\nAs a result, when the `AndroidLibraryModule` component is invoked, it will incorporate all the modifications, including the added `enable_data_binding` argument, as well as the top-level `package` function call.\n\n```python\nload(\"@io_bazel_rules_kotlin//kotlin:android.bzl\", \"kt_android_library\")\nload(\"@rules_jvm_external//:defs.bzl\", \"artifact\")\n\nkt_android_library(\n    name = \"my-library\",\n    srcs = glob([\"src/main/**/*.kt\"]),\n    custom_package = \"com.turo.mylibrary\",\n    manifest = \"src/main/AndroidManifest.xml\",\n    resource_files = glob([\"src/main/res/**\"]),\n    deps = [...],\n    enable_data_binding = True, # added by a feature component \n)\n\n# added by a feature component\npackage(default_visibility = [\"//visibility:public\"])\n```\n\n\u003e Refer to the example of [`JetpackComposeFeature`](https://github.com/Morfly/airin/blob/989df0b03a2a38c4390839104de3b1f248ffcaca/airin-gradle-android/src/main/kotlin/io/morfly/airin/feature/JetpackComposeFeature.kt) to learn more.\n\n\n## Shared components\nThe purpose of shared components is to enable feature components to contribute into multiple module components.\n\nThere could be multiple types of shared components.\n- **Shared module component** — receives contributions from every shared feature component, even if it is not directly included in it.\n- **Shared feature component** — contributes to every shared module component.\n- **Top-level feature component** — does not belong to any module component specifically and contributes to all module components, even non-shared ones.\n\nThis is how they are declared in a Gradle plugin.\n\n```kotlin\n// root build.gradle.kts\nairin {\n  register\u003cAndroidLibraryModule\u003e {\n    // shared feature component\n    include\u003cHiltFeature\u003e { shared = true }\n  }\n\n  // shared module component\n  register\u003cRootModule\u003e { shared = true }\n\n  // top-level feature component\n  include\u003cAllPublicFeature\u003e()\n}\n```\nHere is what's happening in the example above.\n- `HiltFeature` - feature component that configures Hilt for a Bazel module.\n  - Contributes to `AndroidLibraryModule` because they are directly connected. Includes Hilt in Bazel scripts in each Android library module.\n  - Contributes to `RootModule` because they are both shared. Configures Hilt in a Bazel workspace.\n- `AllPublicFeature` - feature component that configures default public visibility in a Bazel file.\n  - contributes to `AndroidLibraryModule` and `RootModule` because it's a top-level feature component.\n\n\u003e Refer to examples of [`AndroidLibraryModule`](https://github.com/Morfly/airin/blob/d2810f569b5da84ec61106a8c85d2b3566b1f7a8/airin-gradle-android/src/main/kotlin/io/morfly/airin/module/AndroidLibraryModule.kt), \n\u003e [`HiltFeature`](https://github.com/Morfly/airin/blob/d2810f569b5da84ec61106a8c85d2b3566b1f7a8/airin-gradle-android/src/main/kotlin/io/morfly/airin/feature/HiltFeature.kt) \n\u003e and [`RootModule`](https://github.com/Morfly/airin/blob/d2810f569b5da84ec61106a8c85d2b3566b1f7a8/airin-gradle-android/src/main/kotlin/io/morfly/airin/module/RootModule.kt) to learn more.\n\n## Properties\nBoth module and feature components offer an API for declaring properties, that serve as arguments that allow additional customization when configuring `airin` plugin.  \n```kotlin\n// root build.gradle.kts\nairin {\n  register\u003cRootModule\u003e {\n    include\u003cAndroidToolchainFeature\u003e {\n      rulesKotlinVersion = \"1.8.1\"\n      rulesKotlinSha = \"a630cda9fdb4f56cf2dc20a4bf873765c41cf00e9379e8d59cd07b24730f4fde\"\n    }\n  }\n}\n```\nTo declare a property in a custom component use `property` delegate and provide a default value.\n```kotlin\nabstract class AndroidToolchainFeature : FeatureComponent() {\n  val rulesKotlinVersion: String by property(default = \"1.8.1\")\n  val rulesKotlinSha: String by property(default = \"a630cda9fdb4f56cf2dc20a4bf873765c41cf00e9379e8d59cd07b24730f4fde\")\n\n  override fun FeatureContext.onInvoke(module: GradleModule) {\n    ...\n  }\n}\n```\n\n\u003e Refer to the example of [`AndroidToolchainFeature`](https://github.com/Morfly/airin/blob/d2810f569b5da84ec61106a8c85d2b3566b1f7a8/airin-gradle-android/src/main/kotlin/io/morfly/airin/feature/AndroidToolchainFeature.kt) to learn more.\n\n### Default properties\nThere are a few default properties that are available in each component.\n- `shared` - makes a component shared, as described in [shared components](#shared-components) section.\n- `ignored` - excludes the component from the migration process.\n- `priority` - applied only to module components and defines the priority of the component. It complements `ComponentConflictResolution.UsePriority`, so that a component with a highest priority is select for a module in case of a conflict.\n\n### Shared properties\nRegular properties are defined during the Gradle configuration phase and are common in all modules that use a certain component.\n\nShared properties, on the other hand, are used during the Gradle execution phase and allow sharing the data between related module and shared components within a single module.\n\n```kotlin\nabstract class HiltFeature : FeatureComponent() {\n    override fun FeatureContext.onInvoke(module: GradleModule) {\n        sharedProperties[\"myProperty\"] = \"value\"\n    }\n}\n```\n\n```kotlin\nabstract class AndroidLibraryModule : FeatureComponent() {\n    override fun FeatureContext.onInvoke(module: GradleModule) {\n        val myProperty = sharedProperties[\"myProperty\"] as String\n    }\n}\n```\n\nThe components are invoked following the order specified in `airin` extension, so that for each module, feature components are invoked prior to the module component.\n## Decorators\nDecorators allow extracting an additional information about Gradle modules that will automatically decorate instances of `GradleModule` in\n`onInvoke` calls of each module and feature component during the migration.\n\nBy default, `io.morfly.airin.android` Gradle plugin employs [`AndroidModuleDecorator`](https://github.com/Morfly/airin/blob/3bad5940d680423d3af130e1053a04b7e7d78a72/airin-gradle-android/src/main/kotlin/io/morfly/airin/AndroidModuleDecorator.kt). \nIts purpose is to provide additional information about modules extracted from Android Gradle plugin, such as package name, enabled build features, etc.\n\nThe simple example of a module decorator could be found below.\n```kotlin\nopen class AndroidModuleDecorator : GradleModuleDecorator {\n\n  override fun GradleModule.decorate(project: Project) { \n    // make sure this is the right type of modules\n    if (!project.plugins.hasPlugin(\"com.android.library\")) return\n        \n    // prepared the additional data\n    val androidMetadata = AndroidMetadata(\n      namespace = extensions.findByType(CommonExtension::class.java)?.namespace\n    )\n      \n    // decorate the module instance\n    this.properties[\"androidMetadata\"] = androidMetadata\n  }\n}\n```\nAfter the decorator is declared, all that is left is to apply it in the `airin` plugin configuration.\n```kotlin\n// root build.gradle.kts\nairin {\n    decorateWith\u003cAndroidModuleDecorator\u003e()\n}\n```\n\nAs a result, you can extract this data in your custom components.\n```kotlin\nabstract class MyFeatureComponent : FeatureComponent() {\n\n    override fun FeatureContext.onInvoke(module: GradleModule) {\n        val myData = module.properties[\"androidMetadata\"]\n    }\n}\n```\n\u003e Refer to the example of [`AndroidModuleDecorator`](https://github.com/Morfly/airin/blob/3bad5940d680423d3af130e1053a04b7e7d78a72/airin-gradle-android/src/main/kotlin/io/morfly/airin/AndroidModuleDecorator.kt) to learn more.\n## License\n\n    Copyright 2023 Pavlo Stavytskyi.\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       https://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmorfly%2Fairin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmorfly%2Fairin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmorfly%2Fairin/lists"}