GithubHelp home page GithubHelp logo

hooks's Introduction

Conan Hooks

logo

Build Status

Repository to develop experimental Conan hooks for Conan >= 1.8.

Hook setup

Place your hook Python files under ~/.conan/hooks. The name of the hook would be the same one as the file name.

*~/.conan/hooks/conan-center.py

Only copying hook files will not activate them.

Conan config as installer

To install all hooks from Conan repository in Github:

$ conan config install https://github.com/conan-io/hooks.git

If you are using Conan >=1.14 you can specify the source and destination folder to avoid copying undesired files to your local cache:

$ conan config install https://github.com/conan-io/hooks.git -sf hooks -tf hooks

Conan config install does not activate any hook.

Hook activation

You can activate any hook with:

$ conan config set hooks.conan-center

If you handle multiple dependencies in your project is better to add a conan.conf:

    [hooks]
    attribute_checker
    conan-center_reviewer

Hooks

These are the hooks currently available in this repository

This hook does checks for the inclusion guidelines of third-party libraries in Conan Center.

It is mostly intended for users who want to contribute packages to Conan Center. With this hook they will test some of the requirements in the guidelines, as this hook will check for recipe metadata, binary matching... during the conan create step and will output the result of each check as OK, WARNING or ERROR:

[HOOK - conan-center_reviewer.py] pre_export(): [RECIPE METADATA] OK
[HOOK - conan-center_reviewer.py] pre_export(): [HEADER ONLY] OK
[HOOK - conan-center_reviewer.py] pre_export(): [NO COPY SOURCE] OK
[HOOK - conan-center_reviewer.py] pre_export(): [FPIC OPTION] OK
[HOOK - conan-center_reviewer.py] pre_export(): [FPIC MANAGEMENT] 'fPIC' option not found
[HOOK - conan-center_reviewer.py] pre_export(): [VERSION RANGES] OK
[HOOK - conan-center_reviewer.py] post_package(): ERROR: [PACKAGE LICENSE] No package licenses found in: ~/
.conan/data/name/version/jgsogo/test/package/3475bd55b91ae904ac96fde0f106a136ab951a5e. Please
 package the library license to a 'licenses' folder
[HOOK - conan-center_reviewer.py] post_package(): [DEFAULT PACKAGE LAYOUT] OK
[HOOK - conan-center_reviewer.py] post_package(): [MATCHING CONFIGURATION] OK
[HOOK - conan-center_reviewer.py] post_package(): [SHARED ARTIFACTS] OK

If you want the hook to fail the execution, if an error is reported, you can adjust the environment variable CONAN_HOOK_ERROR_LEVEL:

  • CONAN_HOOK_ERROR_LEVEL=40 it will raise if any error happen.
  • CONAN_HOOK_ERROR_LEVEL=30 it will raise if any error or warning happen.

Conan 2.x support

The Conan Center hook is NOT supported by Conan v2 yet. Do not try to run this file with Conan v2.

There is an effort on the disabled-hook_conan-center-v2.py, but is not updated and should be broken by now.

The support for Conan 2.x should be rethinked first, because others items like linter and extensions should be considered too.

This hook checks that some important attributes are present in the ConanFile: url, license and description, and will output a warning for the missing ones.

This Conan hook validates produced binary artifacts of the given package.

Binaries stored in the package folder are checked for compatibility with package settings and options.

Currently, the following checks are performed:

  • Binary format (Mach-O, ELF or PE)
  • Architecture
  • No shared libraries are produced for shared=False
  • Visual Studio runtime library in use

The hook uses LIEF library in order to perform its checks.

The hook is automatically called when package command is executed.

This Conan hook reads your recipe and updates its GitHub repository properties using the attributes.

The following attributes are updated:

  • homepage

  • description

  • topics

It's necessary to pass GitHub token by environment variable: GITHUB_TOKEN.

This pre_export() hook checks ConanFile's members for potential typos, for example:

from conans import ConanFile

class ConanRecipe(ConanFile):
    name = "name"
    version = "version"

    export_sources = "OH_NO"

Will produce the next warning:

pre_export(): WARN: The 'export_sources' member looks like a typo. Similar to:
pre_export(): WARN:     exports_sources

This Conan hook validates that conanfile's license attribute specifies valid license identifier(s) from the SPDX license list.

The hook uses spdx_lookup python module in order to perform its checks.

Use pip install spdx_lookup in order to install required dependency.

The hook is automatically called when export command is executed.

This hook runs Pylint over the recipes before exporting them (it runs in the pre_export hook), it can be really useful to check for typos, code flaws or company standards.

There several environment variables you can use to configure it:

  • CONAN_PYLINTRC: path to a configuration file to fully customize Pylint behavior.
  • CONAN_PYLINT_WERR: if set, linting errors will trigger a ConanException.
  • CONAN_PYLINT_RECIPE_PLUGINS: list of modules (comma separated list) to load. They are used to register additional checker or dynamic fields before running the linter. By default it points to the conans.pylint_plugin module distributed together with Conan, this file contains the declaration of some extra fields that are valid in the ConanFile class.

This hook requires additional dependencies to work: pip install pylint astroid.

Separate KB-H047 from Conan Center, which is no longer required due Python 2.7 deprecation.

Validates if conanfile.py and test_package/conanfile.py contain a non-ascii present, when there is a character, it logs an error.

This hook runs yamllint over the yaml files in a recipe before exporting them (it runs in the pre_export hook), it can be really useful to check for typos.

This hook requires additional dependencies to work: pip install yamllint.

Same as KB-H031, but for conan 2.0. Reduces conandata.yml files, keeping only entry for the current package version. It only helps to avoid generation of extra recipe revisions in case of adding new versions.

License

MIT License

hooks's People

Contributors

a4z avatar boussaffawalid avatar cguldner avatar croydon avatar czoido avatar danimtb avatar dmn-star avatar ericlemanissier avatar ericriff avatar estebandugueperoux2 avatar garethsb avatar gokhanettin avatar jgsogo avatar lasote avatar madebr avatar memsharded avatar minimonium avatar ohanar avatar prince-chrismc avatar pro avatar rubenrbs avatar samuel-emrys avatar serpent7776 avatar spaceim avatar sparik avatar sse4 avatar theirix avatar toge avatar uilianries avatar valgur avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hooks's Issues

How to test plugins?

Hi!

It would be nice add some tests for new plugins but using conan config to install, all tests will be copied also.

Is there some way to ignore those files using conan config install ?

Credentials of bintray plugin env vars

About these variables:

CONAN_LOGIN_USERNAME: Bintray login username
CONAN_PASSWORD: Bintray API KEY

I know that these variables are used in CPT, but even these I would like to rename them.
I think would make more sense to name them as "BINTRAY_XXX" even check if there are some "standard" variables maybe in the jfrog cli?

[bintray-update] Wont work with conan_server

Hi!

When running conan_server, the hooks bintray-update shown:

[HOOK - bintray_update.py] post_upload_recipe(): ERROR: Could not extract subject and repo from http://0.0.0.0:9300: Invalid pattern

We need to check before to extract the information if the endpoint is Bintray.

[github-updater] Increase output information

The github-updater hook works silently and in my case it only outputs:

[HOOK - conanio/hooks/github-updater.py] pre_export(): no topics to update

I don't know if it is done on purpose, but it should say what this it is updating in github.

binary sign hook

add hook to sign conan libraries
should invoke signtool for Windows/WindowsStore, and codesign for Macos/iOS/watchOS/tvOS.
probably, should be no-op for other platforms (like Linux).
some environment variables should specify path to the sign keys.

Handle configurations of hooks

As soon as hooks start to proliferate there will be more and more environment variables to configure them, I think we should consider that risk and think about alternatives, there could be a better approach. It is time for brainstorming:

  • Do nothing, hooks will start to introduce env variables (there are already some of them), I think it could be a mess in the long term

  • Use a json file to handle the configuration of all the hooks (shareable with a conan config install), the HookManager could pass those variables in every function call:

    hooks_config.json

    {
        "hook_name_1": {
            "key1": "value",
            "key2": "value"
        },
        "hook_name_2": {
            "...": "..."
        }
    }

    hooks/hook_name_1.py

    ...
    def pre_export(..., config):
        assert config == {"key1": "value", "key2": "value"}
        ...
    
    def post_package(..., config):
        ...
  • add them to conan.conf file inside the cache

  • ...more ideas?

[binary-linter] Reduce verbosity of output

Outputing 2 lines for every file in package folder is too much for a reasonable output:

[HOOK - conanio/hooks/binary-linter.py] post_package(): conan binary linter plug-in
[HOOK - conanio/hooks/binary-linter.py] post_package(): file "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\conaninfo.txt" is not a executable, skipping...
[HOOK - conanio/hooks/binary-linter.py] post_package(): file "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\conanmanifest.txt" is not a executable, skipping...
[HOOK - conanio/hooks/binary-linter.py] post_package(): checking file "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr110.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr110d.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr100.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr100d.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr130.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr130d.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr120.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr120d.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" imports library "vcruntime140.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "vcruntime140d.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" imports library "vcruntime140.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "vcruntime140d.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr90.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr90d.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr80.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msvcr80d.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "cygwin1.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msys-1.0.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\bin\docopt.dll" doesn't import library "msys-2.0.dll"
[HOOK - conanio/hooks/binary-linter.py] post_package(): file "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\include\docopt.h" is not a executable, skipping...
[HOOK - conanio/hooks/binary-linter.py] post_package(): file "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\include\docopt_util.h" is not a executable, skipping...
[HOOK - conanio/hooks/binary-linter.py] post_package(): file "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\include\docopt_value.h" is not a executable, skipping...
[HOOK - conanio/hooks/binary-linter.py] post_package(): file "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\lib\docopt.lib" is not a executable, skipping...
[HOOK - conanio/hooks/binary-linter.py] post_package(): file "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\licenses\LICENSE-Boost-1.0" is not a executable, skipping...
[HOOK - conanio/hooks/binary-linter.py] post_package(): file "C:\Users\danimtb\.conan\data\docopt\0.6.2\danitmb\testing\package\970e773c5651dc2560f86200a4ea56c23f568ff9\licenses\LICENSE-MIT" is not a executable, skipping...

All those is not a executable, skipping... could be omitted

[bintray-update] Map SPDX licenses

Hi!

There is a lack between Bintray and SPDX licenses, where Bintray only supports a limited number of licenses, but any Conan recipe could have one or multiple licenses, following the SPDX format.

This feature intents to map SPDX licenses to Bintray format, fixing possible lacks.
For example:

SPDX GPL-2.0-or-later -> Bintray GPL-2.0

Regards!

conan-center hook fails on symlinks

ERROR: [HOOK - conan-center.py] pre_export(): [Errno 62] Too many levels of symbolic links: '/Users/sse4/bincrafters/test-symlinks/test'
Traceback (most recent call last):
  File "/Users/sse4/bincrafters/conan/conans/client/hook_manager.py", line 58, in execute
    method(output=output, **kwargs)
  File "/Users/sse4/.conan/hooks/conan-center.py", line 58, in wrapper
    ret = func(output, *args, **kwargs)
  File "/Users/sse4/.conan/hooks/conan-center.py", line 142, in pre_export
    @run_test("RECIPE FOLDER SIZE", output)
  File "/Users/sse4/.conan/hooks/conan-center.py", line 67, in tmp
    ret = func(out)
  File "/Users/sse4/.conan/hooks/conan-center.py", line 151, in test
    total_size += os.path.getsize(file_path)
  File "/usr/local/Cellar/python@2/2.7.16/Frameworks/Python.framework/Versions/2.7/lib/python2.7/genericpath.py", line 57, in getsize
    return os.stat(filename).st_size
OSError: [Errno 62] Too many levels of symbolic links: '/Users/sse4/bincrafters/test-symlinks/test'

repro:

mkdir test-symlinks
cd test-symlinks
conan new test/1.0@user/testing
ln -s test test
conan create . user/testing

Documentation about hooks

It may be too early in the process for this, but in artifactory plugins, we've given each plugin its own directory so that its test and readme functions can be clearly defined on an individual plugin-by-plugin basis. This allows us to test one plugin at a time etc.
Also this mandates an individual readme per plugin which tries to summarize what each plugin does, so a user can decide whether or not they want an individual plugin. Given how early we are in building plugins, that may be premature. On the other hand, having been responsible for the effort of creating those readmes a few years later in a repository without that documentation, it is definitely easier for everyone to do it up-front, so I figured I'd mention it now.

[conan center] check meta lines

check the following meta lines are not in conanfile.py and test_package/conanfile.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
  • shebang
  • vim
  • coding header

clarify requirements

this topic actually has multiple questions to clarify

  • can plug-in depend on other python libraries (e.g. lief, requests)? if so, how to specify them (e.g. requirements.txt)?
  • can plug-in have build_requirements? (e.g. if it depend on external tool like git, ninja or whatever)
  • can plug-in have system_requirements? (as above, to install system tools, like git)
  • can plug-in have python_requirements? (to share code between plug-ins)
  • can plug-in have requirements?

UPDATE: it turns out, hooks cannot use any of conan dependencies, like requirements, system_requirements, python_requirements or build_requerements, as they invoke conan methods, which has to be hooked, and it goes into infinite vicious circle.
moreover, hooks should be very careful choosing which conan API to use, in order to avoid such infinite recursion.

/cc @solvingj @Croydon @uilianries @lasote @memsharded @danimtb @jgsogo

Should we rename hooks?

When I started the conan-center hook I thought it would make sense to call it the same way as the remote conan-center.

Having a look at the existing hooks I would like to have the naming "somehow" unified.

This is my proposal:

  • binary-linter.py --> binary_linter.py
  • bintray_update.py --> bintray_updater.py
  • github-updater.py --> github_updater.py

Should we rename conan-center.py to something like conan-center_reviewer.py or conan-center_checker.py ?

cc/ @conan-io/community-leaders

Centrally installed hooks vs. build reproducibility

Continuing discussion from #21 (comment)

In my opinion, hooks, as they are currently, should be akin to git hooks: not necessary for functioning, but allow complementing functionality. They should never affect the resulting build.

If I relied on a hook for capturing some build-time information in my package (as suggested on Slack, see my earlier comment, I would need a specific hook installed to be able to reproduce that package. And that requirement is not apparent from the conanfile (nor is it enforced by conan). My recipe is no longer portable, I need to take specific steps (installing a hook) before I can rebuild it.

Furthermore, it seems to me that once a hook is installed it affects all builds? (I might be wrong about this). This would make it impossible to perform functionality on just a couple of recipes.

I would propose a different, simpler model, based on function overrides. So hook functions like pre_export, post_export etc. would be simply member functions in Conanfile. That would make using the hooks very visible and environment-independent (if I clone the repo on a different machine, I don't need to perform additional hidden steps, it's there in the conanfile). This model also allows global overrides, by relying on python_requires and an intermediary base class:

class MyConanfile(Conanfile):
  def post_source(self):
    print("I'm in all recipes derived from MyConanfile")

Again, this preserves the build reproducibility.

Add Appveyor CI

We need to run tests against Conan versions in Windows too.

[conan-center] settings.os should not be mandatory

Hi!

When building an installer package (bincrafters/pkg-config_installer), only the settings os_build and arch_build are available. However, the hook conan-center requires settings.os as well:

ERROR: [HOOK - conan-center.py] post_build(): 'settings.os' doesn't exist
'settings' possible configurations are ['arch_build', 'compiler', 'os_build']
Traceback (most recent call last):
  File "/opt/python/pyenv/versions/3.6.0/lib/python3.6/site-packages/conans/client/hook_manager.py", line 56, in execute
    method(output, **kwargs)
  File "/home/uilian/.conan/hooks/conan-center.py", line 121, in post_build
    if not _files_match_settings(conanfile, conanfile.build_folder):
  File "/home/uilian/.conan/hooks/conan-center.py", line 209, in _files_match_settings
    os = _get_os(conanfile)
  File "/home/uilian/.conan/hooks/conan-center.py", line 243, in _get_os
    if hasattr(conanfile.settings, attrib):
  File "/opt/python/pyenv/versions/3.6.0/lib/python3.6/site-packages/conans/model/settings.py", line 257, in __getattr__
    self._check_field(field)
  File "/opt/python/pyenv/versions/3.6.0/lib/python3.6/site-packages/conans/model/settings.py", line 253, in _check_field
    raise undefined_field(self._name, field, self.fields, self._parent_value)
conans.errors.ConanException: 'settings.os' doesn't exist
'settings' possible configurations are ['arch_build', 'compiler', 'os_build']

We should check if os or os_build are available.

Regards!

[conan center] check there is no system_requirements

I think we have previously agreed to disallow system_requirements in conan-center-index
so, it would be nice to check in hook there are no system_requirements methods
all exceptions will be white-listed in hook I guess

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.