Giter VIP home page Giter VIP logo

weasel's Introduction

Verifying Licenses

This is an automatic license checker aimed at ensuring compliance with the Apache Software Foundation processes. It does not ensure compliance, but it catches many common errors automatically.

To get started quickly, install weasel with go get:

go get github.com/comcast/weasel

Then, cd into your project's directory and run:

weasel

That will list all the potentially problematic licenses in the project. You'll probably need to configure your .dependency_license file to document the dependencies that can't be detected automatically and suppress the ones that are detected wrongly.

weasel

This is the binary with all the logic in it. weasel will list every file in every subdirectory, starting from the target directory, and determine the most likely license for that file. It errs on the side of false positives, since the consequences of a false negative are considerably more serious.

weasel [-q] [--] <target_dir>:

  • -a Print all files and their licenses, not just problematic files.
  • -q Suppress the printing of non-problematic files. This is the default.
  • -d <sub_dir> Only run on files in the specified subdirectory.
  • -f <out_file> Also write license results to <out_file>.
  • -- Nothing after this is interpreted as an argument.
  • <target_dir> To run weasel against a different target. The target directory must be the root of the project. If it is omitted, weasel will search directories upward from the current directory, looking for a .git folder to indicate the root.

LICENSE

The LICENSE file is at the root of the project (whence you ought to run weasel). This must comply with the requirements of the Apache Software Foundation and is intended for human consumption.

Nevertheless, with a bit of careful writing, it's possible to have weasel help verify that everything gets covered.

Lines that begin with an @-symbol are interpreted as a path specification that describes a set of files covered by the license. weasel does not validate that @ files are actually licensed correctly, merely that they are mentioned. This covers the most common case, which is adding a (even potentially correctly licensed!) file and forgetting to mention it in the LICENSE file.

Likewise, it's impermissible to use an @-line that describes no files. This usually happens when a dependency is removed and the LICENSE file does not get updated properly.

@-lines are interpreted by path.Match, the syntax for which is:

pattern:
    { term }
term:
    '*'         matches any sequence of non-/ characters
    '?'         matches any single non-/ character
    '[' [ '^' ] { character-range } ']'
                character class (must be non-empty)
    c           matches character c (c != '*', '?', '\\', '[')
    '\\' c      matches character c

character-range:
    c           matches character c (c != '\\', '-', ']')
    '\\' c      matches character c
    lo '-' hi   matches character c for lo <= c <= hi

.dependency_license

Sometimes, there's no reasonable way to automatically detect the appropriate license for a file, especially files that don't support comments or are binary. .dependency_license allows you to document the exceptions so that new files show up clearly.

The .dependency_license must appear in the root of the project.

Each line should either be empty, a comment (prepended by an octothorp), or a license exception line. A license exception line is a regular expression, a comma, then the name of a license, then optionally an octothorp followed by a comment (which may not contain a comma!).

license-exception:
    regex ',' license-name [ '#' { commentable-char } ]       Associates the license with the file.        
    regex ',' '!' license-name [ '#' { commentable-char } ]   Disassociates the license from the file.

regex: A regular expression accepted by golang regexps, described here: https://golang.org/s/re2syntax

license-name:
    'Apache'    Apache License
    'BSD'       Berkeley Software Distribution License
    'MIT'       Massachusetts Institute of Technology License
    'GoBSD'     BSD-style license used by the GoLang team
    'ISC'       Internet Systems Consortium
    'X11'       MIT License, by an older name.
    'WTFPL'     Do What the Fuck You Want to Public License
    'GPL/LGPL'  Either the GNU General Public License or the GNU Lesser General Public License
    'PD'        Public Domain
    'Docs'      A documentation file
    'Empty'     An empty file
    'Ignored'   A file that ought not be analyzed for compliance

commentable-char: Any character other than a ','

You can also create a .dependency_licenses directory, and all files inside will be used as overrides, with their paths applied to the parent directory.

Output

weasel prints out problematic files with an exclamation point after the license name. If no licensing or file information could be determined, Unknown! is printed as the license. If the general file type is determined Unknown-type! is used as the license.

If weasel determines the type of the file and the type isn't Apache-2.0, then it needs to appear explicitly in the LICENSE file. So a ! after a recognized license means that the file doesn't match one of the entries in the main LICENSE file.

If the license contains a ~, it means licensing was determined by looking at a LICENSE file in the same directory. This helps with licensing for vendored dependencies.

Docker Image

A docker image is published for weasel to facilitate use on platforms without a functional go installation and for users that do not wish to compile it. You can get up and going immediately with weasel like this:

docker run --rm -v $(git rev-parse --show-toplevel 2>/dev/null || pwd):/src licenseweasel/weasel /src

Since weasel is running in a container, you have to mount a volume with the source in it. You can also use weasel via docker-compose.

Docker Image Licensing

Source code for weasel is licensed under Apache-2.0; available in the LICENSE file in this repository.

The image is built from scratch and includes git and it's dependencies glibc and zlib. Their licenses can be found here:

You can export the sources tarball with full source for the dependency from the source image (licenseweasel/weasel-src) with:

export TMP_CONTAINER="$(docker create licenseweasel/weasel-src:latest)"
docker export $TMP_CONTAINER | tar -x sources.tgz
docker rm $TMP_CONTAINER

Full licenses for these components are included in the source tarball.

GitHub Action

weasel runs as a GitHub action as-is. You can easily and automatically run it on pushes and pull request by creating a file at .github/workflows/weasel.yml with these contents:

name: Weasel License Validation
on:
  push:
  create:
  pull_request:
    types:
    - opened
    - reopened
    - edited
    - synchronize
jobs:
  weasel:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v2
    - name: Run weasel
      uses: docker://licenseweasel/weasel:v0.4

Of course, be sure to put the appropriate licensing information for your project as necessary.

Best Practices

License management can be tricky at the best of times. The goal of this tool is to automate as much of that as possible. Here are some best practices:

  • weasel before you commit. If it prints anything, you probably need to add license headers.
  • Do not Ignore files. If it's reasonable to quiet weasel about a false positive or negative in another way, do that instead.
  • If an unrecognized file has a header, update weasel, not .dependency_license. It's relatively straightforward to add license recognition to licenseList.go. Doing it that way benefits future files as well.
  • Run weasel as part of Continuous Integration. Issues are not usually difficult to fix, but automatic running allows them to be fixed promptly. If you use GitHub, see above for easy integration with GitHub Actions.

Building Weasel Binaries

  1. Ensure the VERSION file reflects the target release version info
  2. docker-compose -f docker-compose.build_bin.yml build --no-cache && docker-compose -f docker-compose.build_bin.yml up

Your ./dist directory should be filled with archives of binaries and source tarballs

weasel's People

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

weasel's Issues

can't load package: package weasel/dedup: cannot find package "weasel/dedup"

Building the Docker image fails. Running docker build . at 9ea2ab6 (v0.4) yields this error:

#9 [build-weasel 4/5] RUN CGO_ENABLED=0 go install weasel
#9 DONE 18.7s

#10 [build-weasel 5/5] RUN CGO_ENABLED=0 go install weasel/dedup
#10 0.954 can't load package: package weasel/dedup: cannot find package "weasel/dedup" in any of:
#10 0.954 	/usr/local/go/src/weasel/dedup (from $GOROOT)
#10 0.954 	/go/src/weasel/dedup (from $GOPATH)
#10 ERROR: executor failed running [/bin/sh -c CGO_ENABLED=0 go install weasel/dedup]: runc did not terminate sucessfully

#15 [build-git 3/7] RUN apt-get -y update && apt-get -y install git build-es...
#15 CANCELED
------
 > [build-weasel 5/5] RUN CGO_ENABLED=0 go install weasel/dedup:
------
failed to solve with frontend dockerfile.v0: failed to build LLB: executor failed running [/bin/sh -c CGO_ENABLED=0 go install weasel/dedup]: runc did not terminate sucessfully
Full docker build log (click to expand)
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 38B done
#1 DONE 0.1s

#2 [internal] load .dockerignore
#2 transferring context: 2B done
#2 DONE 0.1s

#4 [internal] load metadata for docker.io/library/golang:1.10-alpine
#4 ...

#3 [internal] load metadata for docker.io/library/debian:latest
#3 DONE 16.7s

#4 [internal] load metadata for docker.io/library/golang:1.10-alpine
#4 DONE 16.8s

#5 [build-weasel 1/5] FROM docker.io/library/golang:1.10-alpine@sha256:5495...
#5 DONE 0.0s

#13 [build-git 1/7] FROM docker.io/library/debian@sha256:46d659005ca1151087e...
#13 DONE 0.0s

#14 [build-git 2/7] RUN sed -i 's/^deb\(.*\)$/deb\1\ndeb-src\1/' /etc/apt/so...
#14 CACHED

#7 [internal] load build context
#7 transferring context: 7.40kB 0.1s done
#7 DONE 0.2s

#6 [build-weasel 2/5] WORKDIR /go/src/weasel
#6 CACHED

#15 [build-git 3/7] RUN apt-get -y update && apt-get -y install git build-es...
#15 ...

#8 [build-weasel 3/5] COPY . .
#8 DONE 0.5s

#9 [build-weasel 4/5] RUN CGO_ENABLED=0 go install weasel
#9 ...

#15 [build-git 3/7] RUN apt-get -y update && apt-get -y install git build-es...
#15 8.825 Get:1 http://security.debian.org/debian-security buster/updates InRelease [65.4 kB]
#15 8.829 Get:2 http://deb.debian.org/debian buster InRelease [121 kB]
#15 9.015 Get:3 http://deb.debian.org/debian buster-updates InRelease [51.9 kB]
#15 9.112 Get:4 http://security.debian.org/debian-security buster/updates/main Sources [129 kB]
#15 9.157 Get:5 http://security.debian.org/debian-security buster/updates/main amd64 Packages [204 kB]
#15 9.197 Get:6 http://deb.debian.org/debian buster/main Sources [7831 kB]
#15 9.415 Get:7 http://deb.debian.org/debian buster/main amd64 Packages [7905 kB]
#15 9.599 Get:8 http://deb.debian.org/debian buster-updates/main Sources [3716 B]
#15 9.599 Get:9 http://deb.debian.org/debian buster-updates/main amd64 Packages [7868 B]
#15 11.31 Fetched 16.3 MB in 11s (1550 kB/s)
#15 11.31 Reading package lists...
#15 11.84 Reading package lists...
#15 12.34 Building dependency tree...
#15 12.42 Reading state information...
#15 12.51 The following additional packages will be installed:
#15 12.51   binutils binutils-common binutils-x86-64-linux-gnu bzip2 ca-certificates cpp
#15 12.51   cpp-8 curl dirmngr dpkg-dev fakeroot g++ g++-8 gcc gcc-8 gettext-base
#15 12.51   git-man gnupg gnupg-l10n gnupg-utils gpg gpg-agent gpg-wks-client
#15 12.51   gpg-wks-server gpgconf gpgsm krb5-locales less libalgorithm-diff-perl
#15 12.51   libalgorithm-diff-xs-perl libalgorithm-merge-perl libasan5 libassuan0
#15 12.51   libatomic1 libbinutils libbsd0 libc-dev-bin libc6-dev libcc1-0 libcroco3
#15 12.51   libcurl3-gnutls libcurl4 libdpkg-perl libedit2 liberror-perl libexpat1
#15 12.51   libfakeroot libfile-fcntllock-perl libgcc-8-dev libgdbm-compat4 libgdbm6
#15 12.51   libglib2.0-0 libglib2.0-data libgomp1 libgpm2 libgssapi-krb5-2 libicu63
#15 12.51   libisl19 libitm1 libk5crypto3 libkeyutils1 libkrb5-3 libkrb5support0
#15 12.51   libksba8 libldap-2.4-2 libldap-common liblocale-gettext-perl liblsan0
#15 12.51   libmpc3 libmpfr6 libmpx2 libncurses6 libnghttp2-14 libnpth0 libpcre2-8-0
#15 12.51   libperl5.28 libpsl5 libquadmath0 libreadline7 librtmp1 libsasl2-2
#15 12.51   libsasl2-modules libsasl2-modules-db libsqlite3-0 libssh2-1 libssl1.1
#15 12.51   libstdc++-8-dev libtsan0 libubsan1 libx11-6 libx11-data libxau6 libxcb1
#15 12.51   libxdmcp6 libxext6 libxml2 libxmuu1 linux-libc-dev lsb-base make manpages
#15 12.51   manpages-dev netbase openssh-client openssl patch perl perl-modules-5.28
#15 12.51   pinentry-curses publicsuffix readline-common shared-mime-info xauth
#15 12.51   xdg-user-dirs xz-utils
#15 12.51 Suggested packages:
#15 12.51   binutils-doc bzip2-doc cpp-doc gcc-8-locales dbus-user-session
#15 12.51   libpam-systemd pinentry-gnome3 tor debian-keyring g++-multilib
#15 12.51   g++-8-multilib gcc-8-doc libstdc++6-8-dbg gcc-multilib autoconf automake
#15 12.51   libtool flex bison gdb gcc-doc gcc-8-multilib libgcc1-dbg libgomp1-dbg
#15 12.51   libitm1-dbg libatomic1-dbg libasan5-dbg liblsan0-dbg libtsan0-dbg
#15 12.51   libubsan1-dbg libmpx2-dbg libquadmath0-dbg gettext-doc autopoint
#15 12.51   libasprintf-dev libgettextpo-dev git-daemon-run | git-daemon-sysvinit
#15 12.51   git-doc git-el git-email git-gui gitk gitweb git-cvs git-mediawiki git-svn
#15 12.51   parcimonie xloadimage scdaemon glibc-doc libcurl4-doc libidn11-dev
#15 12.51   libkrb5-dev libldap2-dev librtmp-dev libssh2-1-dev pkg-config sensible-utils
#15 12.51   bzr gdbm-l10n gpm krb5-doc krb5-user libsasl2-modules-gssapi-mit
#15 12.51   | libsasl2-modules-gssapi-heimdal libsasl2-modules-ldap libsasl2-modules-otp
#15 12.51   libsasl2-modules-sql libssl-doc libstdc++-8-doc make-doc man-browser
#15 12.51   keychain libpam-ssh monkeysphere ssh-askpass ed diffutils-doc perl-doc
#15 12.51   libterm-readline-gnu-perl | libterm-readline-perl-perl libb-debug-perl
#15 12.51   liblocale-codes-perl pinentry-doc readline-doc
#15 13.18 The following NEW packages will be installed:
#15 13.18   binutils binutils-common binutils-x86-64-linux-gnu build-essential bzip2
#15 13.18   ca-certificates cpp cpp-8 curl dirmngr dpkg-dev fakeroot g++ g++-8 gcc gcc-8
#15 13.18   gettext gettext-base git git-man gnupg gnupg-l10n gnupg-utils gpg gpg-agent
#15 13.18   gpg-wks-client gpg-wks-server gpgconf gpgsm krb5-locales less
#15 13.18   libalgorithm-diff-perl libalgorithm-diff-xs-perl libalgorithm-merge-perl
#15 13.18   libasan5 libassuan0 libatomic1 libbinutils libbsd0 libc-dev-bin libc6-dev
#15 13.18   libcc1-0 libcroco3 libcurl3-gnutls libcurl4 libcurl4-openssl-dev
#15 13.18   libdpkg-perl libedit2 liberror-perl libexpat1 libexpat1-dev libfakeroot
#15 13.18   libfile-fcntllock-perl libgcc-8-dev libgdbm-compat4 libgdbm6 libglib2.0-0
#15 13.18   libglib2.0-data libgomp1 libgpm2 libgssapi-krb5-2 libicu63 libisl19 libitm1
#15 13.18   libk5crypto3 libkeyutils1 libkrb5-3 libkrb5support0 libksba8 libldap-2.4-2
#15 13.18   libldap-common liblocale-gettext-perl liblsan0 libmpc3 libmpfr6 libmpx2
#15 13.18   libncurses6 libnghttp2-14 libnpth0 libpcre2-8-0 libperl5.28 libpsl5
#15 13.18   libquadmath0 libreadline7 librtmp1 libsasl2-2 libsasl2-modules
#15 13.18   libsasl2-modules-db libsqlite3-0 libssh2-1 libssl-dev libssl1.1
#15 13.18   libstdc++-8-dev libtsan0 libubsan1 libx11-6 libx11-data libxau6 libxcb1
#15 13.18   libxdmcp6 libxext6 libxml2 libxmuu1 linux-libc-dev lsb-base make manpages
#15 13.18   manpages-dev netbase openssh-client openssl patch perl perl-modules-5.28
#15 13.18   pinentry-curses publicsuffix readline-common shared-mime-info xauth
#15 13.18   xdg-user-dirs xz-utils zlib1g-dev
#15 ...

#9 [build-weasel 4/5] RUN CGO_ENABLED=0 go install weasel
#9 DONE 18.7s

#10 [build-weasel 5/5] RUN CGO_ENABLED=0 go install weasel/dedup
#10 0.954 can't load package: package weasel/dedup: cannot find package "weasel/dedup" in any of:
#10 0.954 	/usr/local/go/src/weasel/dedup (from $GOROOT)
#10 0.954 	/go/src/weasel/dedup (from $GOPATH)
#10 ERROR: executor failed running [/bin/sh -c CGO_ENABLED=0 go install weasel/dedup]: runc did not terminate sucessfully

#15 [build-git 3/7] RUN apt-get -y update && apt-get -y install git build-es...
#15 CANCELED
------
 > [build-weasel 5/5] RUN CGO_ENABLED=0 go install weasel/dedup:
------
failed to solve with frontend dockerfile.v0: failed to build LLB: executor failed running [/bin/sh -c CGO_ENABLED=0 go install weasel/dedup]: runc did not terminate sucessfully

run on subdirectory of target

This can run quite a while on a large project. After repairing a problem that was found (or multiple problems from a single subdirectory), I should be able to run just on that file or subdirectory with a very short turnaround time.

Running on single sub-directory on Windows yields "Unknown-Executable!" error

On Windows (from a cmd shell) I'm running weasel like

C:\Users\sebastian\Downloads\weasel-0.0.3-windows-amd64>weasel.exe -a -d . C:\Dev\oss-review-toolkit

to only scan the root of C:\Dev\oss-review-toolkit, which yields

In directory: C:\Dev\oss-review-toolkit
Error                                 Unknown! ..\..\Users\sebastian\Downloads\weasel-0.0.3-windows-amd64\weasel.exe

Support for nested .dependency_license files

A .dependency_license file should be able to apply to its whole subdirectory directly. At the least, a master .dependency_license file should be able to incorporate it by reference. This is tricky, though, because the sub-file needs to "see" the paths from it's relative directory.

create dir for output file

if weasel is passed an output file in a directory, it should attempt to create the directory if it does not exist.

License named COPYING is unrecognized although specified

In LICENSE file:

For the  BurntSushi/toml component:
@traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/*
./traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/COPYING

Weasel output:

2020-10-23T07:11:45.9391483Z Error                                 Unknown! traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/decode.go
2020-10-23T07:11:45.9393490Z Error                                 Unknown! traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/decode_meta.go
2020-10-23T07:11:45.9395203Z Error                                 Unknown! traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/doc.go
2020-10-23T07:11:45.9397149Z Error                                 Unknown! traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/encode.go
2020-10-23T07:11:45.9398890Z Error                                 Unknown! traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/encoding_types.go
2020-10-23T07:11:45.9400643Z Error                                 Unknown! traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/encoding_types_1.1.go
2020-10-23T07:11:45.9407106Z Error                                 Unknown! traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/lex.go
2020-10-23T07:11:45.9409037Z Error                                 Unknown! traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/parse.go
2020-10-23T07:11:45.9410998Z Error                                 Unknown! traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/type_check.go
2020-10-23T07:11:45.9412732Z Error                                 Unknown! traffic_ops/traffic_ops_golang/vendor/github.com/BurntSushi/toml/type_fields.go

It looks like there are a fixed set of names that can qualify as a license filename.

for _, licName := range []string{`LICENSE`, `LICENCE`, `LICENSE.md`, `LICENCE.md`, `LICENSE.txt`, `LICENCE.txt`} {

But if the license filename is specified, shouldn't Weasel use the name given?

Integrating with WAMPiS

I'm trying to integrate this checker into the WAMPiS work flow on my department's shared license DB, but I can't seem to get the dependencies to download with my current account. I'm signed in as USER: Common14 Password: Tsacmoc1. Is there a step that's not documented? #XVLF#

Publish binary releases

Please publish binary releases for Linux, Windows and macOS. That would make it much easier for non-developers to try out the tool, and we'd be able to easily integrate it into our OSS Review Toolkit.

Weasel panics if a file is removed before it completes

Testcase (run against a reasonably large repo like apache/trafficcontrol):

set -o monitor #enable job control
file=an-arbitrary-file;
touch $file;
weasel&
echo sleeping;
sleep 2;
rm $file;
echo removed file;
fg;

Stack trace:

sleeping
removed file
weasel
panic: Failed when enumerating working directory: lstat an-arbitrary-file: no such file or directory

goroutine 1 [running]:
main.loadOverrideFile(0xc007fea480, 0x13, 0x0)
        /home/user/go/src/github.com/comcast/weasel/override.go:125 +0x78f
main.loadOverrides.func1(0xc007fea480, 0x13, 0x5e1da0, 0xc0046b04e0, 0x0, 0x0, 0x4ba64f, 0xc0046b04e0)
        /home/user/go/src/github.com/comcast/weasel/override.go:40 +0x11e
path/filepath.walk(0xc007fea480, 0x13, 0x5e1da0, 0xc0046b04e0, 0x5b4b50, 0x0, 0x0)
        /usr/lib/go/src/path/filepath/path.go:360 +0x425
path/filepath.walk(0x5a4d04, 0x1, 0x5e1da0, 0xc0046b0410, 0x5b4b50, 0x0, 0x54943d)
        /usr/lib/go/src/path/filepath/path.go:384 +0x2ff
path/filepath.Walk(0x5a4d04, 0x1, 0x5b4b50, 0x5d98ab, 0x0)
        /usr/lib/go/src/path/filepath/path.go:406 +0xff
main.loadOverrides()
        /home/user/go/src/github.com/comcast/weasel/override.go:32 +0x42
main.main()
        /home/user/go/src/github.com/comcast/weasel/license.go:176 +0x3f9

Noticed from observing a race condition that the Traffic Control Pull Request Builder Jenkins job faces when running against apache/trafficcontrol#4758, depending on whether BUILD_NUMBER exists.

Edit: Permalink to Unknown! BUILD_NUMBER example
https://builds.apache.org/job/trafficcontrol-PR/6002/console (fails: Unknown! BUILD_NUMBER)
https://builds.apache.org/job/trafficcontrol-PR/6003/console (succeeds and BUILD_NUMBER is gitignored)
https://builds.apache.org/job/trafficcontrol-PR/6005/consoleFull (panics, BUILD_NUMBER is still gitignored)

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.