Giter Site home page Giter Site logo

anki-android-backend's Introduction

AnkiDroid-Backend

An interface for accessing Anki Desktop's Rust backend inside AnkiDroid. This allows AnkiDroid to re-use the computer version's business logic and webpages, instead of having to reimplement them.

This is a separate repo that gets published to a library that AnkiDroid consumes, so that AnkiDroid development is possible without a Rust toolchain installed.

Prerequisites

We assume you already have Android Studio, and are able to build the AnkiDroid project already.

The repos Anki-Android and Anki-Android-Backend should be cloned inside the same folder. Furthermore, Anki-Android-Backend should not be renamed, as this name is hard-coded in AnkiDroid gradle files. Unless stated otherwise, all commands below are supposed to be executed in the current repo.

Download Anki submodule

git submodule update --init

C toolchain

Install Xcode/Visual Studio if on macOS/Windows.

Rust

Install rustup from https://rustup.rs/

Ninja

Anki can be built with Ninja or N2. N2 gives better status output and may be installed like so:

./anki/tools/install-n2

On Windows:

bash anki/tools/install-n2

Note: n2 receives occasional mandatory updates. If you see build errors, you may need to re-run this command and re-try the build

NDK

In Android Studio, choose the Tools>SDK Manager menu option.

  • In SDK tools, enable "show package details"
  • Choose NDK version 27.0.12077973
  • After downloading, you may need to restart Android Studio to get it to synchronize gradle.

Windows: msys2

Install msys2 into the default folder location.

After installation completes, run msys2, and run the following command:

pacman -S git rsync

When following the build steps below, make sure msys is on the path:

set PATH=%PATH%;c:\msys64\usr\bin

Building

Two main files need to be built:

  • The main .aar file, which contains the backend Kotlin code, web assets, and Anki backend code compiled for Android.
  • A .jar that contains the backend code compiled for the host platform, for use with Robolectric unit tests.

You should do the first build with the provided shell .sh/.bat file, as it will take care of downloading the target architecture library as well. You'll need to tell the script to use the Java libraries and NDK downloaded by Android Studio, eg on Linux:

export ANDROID_SDK_ROOT=$HOME/Android/Sdk
export ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/27.0.12077973

Or macOS:

export ANDROID_SDK_ROOT=$HOME/Library/Android/sdk
export ANDROID_NDK_HOME=$HOME/Library/Android/sdk/ndk/27.0.12077973

Or Windows using Powershell:

$env:ANDROID_NDK_HOME="$env:ANDROID_SDK_ROOT\ndk\27.0.12077973"

If you don't have Java installed, you may be able to use the version bundled with Android Studio. Eg on macOS:

export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"

or Windows:

set JAVA_HOME=C:\Program Files\Android\Android Studio\jre

Now build with ./build.sh or build.bat.

After you've confirmed building works, you may want to build again with the env var RELEASE=1 defined, to build a faster version.

Modify AnkiDroid to use built library

Now open the AnkiDroid project in AndroidStudio. To tell gradle to load the compiled .aar and .jar files from disk, edit local.properties in the AnkiDroid repo, and add the following line:

local_backend=true

Check Anki-AndroidBackend/gradle.properties's BACKEND_VERSION and Anki-Android/build.gradle's ext.ankidroid_backend_version. Both variables should have the same value. If it is not the case, you must edit Anki-Android's one.

After making the change, you should be able to build and run the project on an x86_64 emulator/device (arm64 on M1 Macs), and run unit tests.

Release builds

Only the current platform is built by default. In CI, the .aar and .jar files are built for multiple platforms, so one release library can be used on a variety of devices. See [.github/workflows] for how this is done.

Testing with a specific version of anki

In this section, we'll consider that you want to test AnkiDroid with the version of Anki at commit $COMMIT_IDENTIFIER from the repository some_repo.

Most of the time $SOME_REPO will simply be origin, that is, ankitects official repository, and COMMIT_IDENTIFIER could be replaced by the tag of the latest stable release. You can find the latest tag by running git tag|sort -V|tail -n1 in the anki directory.

  1. run cd anki to change into the anki submodule directory,
  2. run git fetch $SOME_REPO to ensure you obtain the latest change from this repo.
  3. run git checkout $COMMIT_IDENTIFIER --recurse-submodules to obtain the version of the code at this particular commit.

Creating and Publishing a release

Let's now consider that you want to release a new version of the back-end.

  1. Find the latest stable version of Anki. You can find the latest tag by running git tag|sort -V|tail -n1 in the anki directory. Let's call it version $ANKI_VERSION.
  2. Ensure you are testing and building the back-end against this version (see preceding section to learn how to do it).
  3. In Anki-Android-Backend/gradle.properties you will need to update VERSION_NAME. Its value is of the form $BACKEND_VERSION-$ANKI_VERSION. $ANKI_VERSION should be as defined above. $BACKEND_VERSION should be incremented compared to the last release.
  4. Run the Github workflow Build release (from macOS) manually with a string argument (I typically use shipit, but any string will work) - this will trigger a full release build ready for upload to maven.
  5. Check the workflow logs for the link to Maven Central where if you have a Maven Central user with permissions (like David A and Mike H - ask if you want permission) you may "close" the repository" then after a short wait "release" the repository.
  6. Head over to the main Anki-Android repository and update the AnkiDroid/build.gradle file there to adopt the new backend version once it shows up in https://repo1.maven.org/maven2/io/github/david-allison/anki-android-backend/

Architecture

See ARCHITECTURE.md

License

GPL-3.0 License
AGPL-3.0 Licence (anki submodule)

anki-android-backend's People

Contributors

abdnh avatar arthur-milchior avatar brayandso avatar dae avatar david-allison avatar dependabot-preview[bot] avatar dependabot[bot] avatar gradle-update-robot avatar krmanik avatar mikehardy avatar tarekkma avatar voczi 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  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

anki-android-backend's Issues

chore: Build sqlite.proto in Anki-Android-Backend

This removes an AnkiDroid-specific feature from /anki, which moves it closer to upstream and means we don't need to rebuild the library when making an AnkiDroid specific change.

  • Add protobuf generation code into rslib-bridge/build.rs
  • Enable building Java-based protobufs from Anki-Android-Backend

Add installation instructions

Consider a doctor.sh script

  • Java
  • NDK
  • Python
    • and google protobuf compiler
  • Rust
    • 4 Android targets
    • no spaces in NDK path (Windows)
    • 2/3 targets for testing & warn on macOS
    • windows compiler for testing
    • Nightly for testing

Provide sources for protobuf-generated files in Build Directory

In particular: RustBackend is auto-generated and documentation is vital in understanding the parameters.

We currently see a decompilation:

UInt32 findAndReplace(List<Long> var1, @Nullable String var2, @Nullable String var3, boolean var4, boolean var5, @Nullable String var6);

whereas we should see:

Backend.UInt32 findAndReplace(List<Long> nids, @Nullable String search, @Nullable String replacement, boolean regex, boolean matchCase, @Nullable String fieldName);

Note: The decompilation is seen when the library is obtained from files() - confirm if this is the same if obtained from Maven

Include Protobuf libraries in gradle

If not imported by dependencies, leads to:

	at org.robolectric.android.internal.AndroidTestEnvironment.checkStateAfterTestFailure(AndroidTestEnvironment.java:502)
	at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:581)
	at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:263)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.NoClassDefFoundError: com/google/protobuf/InvalidProtocolBufferException
	at net.ankiweb.rsdroid.BackendFactory.getBackend(BackendFactory.java:40)
	at net.ankiweb.rsdroid.database.RustSupportSQLiteOpenHelper.createRustSupportSQLiteDatabase(RustSupportSQLiteOpenHelper.java:90)
	at net.ankiweb.rsdroid.database.RustSupportSQLiteOpenHelper.getWritableDatabase(RustSupportSQLiteOpenHelper.java:72)
	at com.ichi2.libanki.DB.<init>(DB.java:83)
	at com.ichi2.libanki.backend.RustDroidBackend.openCollectionDatabase(RustDroidBackend.java:39)
	at com.ichi2.libanki.Storage.Collection(Storage.java:76)
	at com.ichi2.anki.CollectionHelper.getCol(CollectionHelper.java:129)
	at com.ichi2.anki.RobolectricTest.getCol(RobolectricTest.java:240)
	at com.ichi2.async.CollectionTaskCheckDatabaseTest.lockDatabase(CollectionTaskCheckDatabaseTest.java:45)
	at com.ichi2.async.CollectionTaskCheckDatabaseTest.checkDatabaseWithLockedCollectionReturnsLocked(CollectionTaskCheckDatabaseTest.java:33)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:575)
	... 6 more
Caused by: java.lang.ClassNotFoundException: com.google.protobuf.InvalidProtocolBufferException
	at org.robolectric.internal.bytecode.SandboxClassLoader.getByteCode(SandboxClassLoader.java:163)
	at org.robolectric.internal.bytecode.SandboxClassLoader.maybeInstrumentClass(SandboxClassLoader.java:129)
	at org.robolectric.internal.bytecode.SandboxClassLoader.lambda$loadClass$0(SandboxClassLoader.java:115)
	at org.robolectric.util.PerfStatsCollector.measure(PerfStatsCollector.java:53)
	at org.robolectric.internal.bytecode.SandboxClassLoader.loadClass(SandboxClassLoader.java:115)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:352)
	... 28 more

May not be required if obtained by Maven.

CI Failure - Javadoc issues

Error reading file: /usr/local/lib/android/sdk/docs/reference/element-list

javadoc: error - Error reading file: /usr/local/lib/android/sdk/docs/reference/element-list
> Task :rsdroid:androidJavadocs
1 error


> Task :rsdroid:androidJavadocs FAILED

Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/6.1.1/userguide/command_line_interface.html#sec:command_line_warnings
18 actionable tasks: 5 executed, 13 up-to-date
FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':rsdroid:androidJavadocs'.
> Javadoc generation failed. Generated Javadoc options file (useful for troubleshooting): '/home/runner/work/Anki-Android-Backend/Anki-Android-Backend/rsdroid/build/tmp/androidJavadocs/javadoc.options'

Remove MemoryHeavySQLiteCursor

This must happen before publishing the first release - I expect this will likely crash low-memory devices for some operations.

rslib does not stream data from SQL currently, all data is loaded into memory, encoded into JSON, and sent to the Python.

https://github.com/ankitects/anki/blob/65d3a1393cbd111861221774f200f51b6ab3e89c/pylib/rsbridge/lib.rs#L81-L93

Rust can afford to avoid streaming SQL due to Rust avoiding the Java heap limit, Java doesn't have the luxury, so we should ideally stream the data.

Implement RustSupportSQLiteDatabase:isLowMemory to enable LimitOffsetSQLiteCursor

Requires ActivityManager or similar to detect:

  • Low RAM Device
  • Current device has low RAM

Which requires a Context object.

RustSupportSQLiteDatabase is created by RustSupportSQLiteOpenHelper.

We can inject a Context there (or an interface abstracting it) into BackendFactory.

This feels easy/somewhat reasonable architecturally (only a couple of levels of indirection), but I'll hold off in case there are better options. It feels rather "enterprise" with the layers of abstraction, and there's probably a more clean way.

Implement CI: Artifact publishing

This will require a little thought (and likely manual work initially):

  • Versioning (alpha|beta|stable)
  • Publishing both rsdroid and rsdroid-testing at the same version number

Mark library as stable

  • Retarget branch to "main"
  • Ensure PRs to main
    • Fix links (for example README has hardcoded links)

Run CI using docker for cross compilation.

CI should use docker+cross to compile binaries.

What I have found: (will get to complete this later :D )

  • GitHub action have docker working on all OS's but macOS
  • ....
  • ....
  • ...

macos_install.yml: Fix "Install NDK" step

We currently use the preinstalled NDK to get around this.

An install currently returns the following error.

Appears to be due to removed Java EE libraries in later versions of Java.

Steps on: https://stackoverflow.com/questions/46402772/failed-to-install-android-sdk-java-lang-noclassdeffounderror-javax-xml-bind-a did not resolve the issue.

java -version and javac -version return 1.8.0_275

Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema
	at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156)
	at com.android.repository.api.SchemaModule.<init>(SchemaModule.java:75)
	at com.android.sdklib.repository.AndroidSdkHandler.<clinit>(AndroidSdkHandler.java:81)
	at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:73)
	at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:48)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlSchema
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:602)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
	... 5 more

Protobuf validation: handle and test `.parseFrom()` failure

The pattern:

        try {
            Pointer backendPointer = ensureBackend();
            byte[] result = NativeMethods.executeCommand(backendPointer.toJni(), 1, Backend.Empty.getDefaultInstance().toByteArray());
            Backend.Progress message = Backend.Progress.parseFrom(result);
            validateMessage(result, message);
            return message;
        } catch (InvalidProtocolBufferException ex) {
            throw BackendException.fromException(ex);
        }

used throughout the generated RustBackendImpl does not properly account for some errors. Leading to:

java.lang.RuntimeException: com.google.protobuf.InvalidProtocolBufferException: Protocol message end-group tag did not match expected tag..

The protobuf generation should be fixed to perform a similar function to validateMessage in the InvalidProtocolBufferException block.

@Nullable in backend methods

I noticed in your recent docs that the backend methods are declared with nullable args, eg:

799600f#diff-427e3c3bcf0309f9e862bb317502e4fc9cb32655a58df4ec9332ed08b36e88c3R79

Was there a particular reason for taking that approach? It is valid protobuf not to set them, and the backend shouldn't fall over if they are not set, but in most cases arguments are expected to be set to something, and a null argument is probably indicative of a bug in the calling code. The way the Python code does it is to have all arguments of the autogenerated methods be required values, and it then becomes the responsibility of the wrapper code to explicitly provide default values for calls where it makes sense to - perhaps such an approach would work in Java too?

Not a high priority, just wanted to bring it up. And congratulations on getting to this point - it looks like you've put in a mammoth effort.

Deprecation: rsdroid - variant.getJavaCompile()

API 'variant.getJavaCompile()' is obsolete and has been replaced with 'variant.getJavaCompileProvider()'.
It will be removed in version 5.0 of the Android Gradle plugin.
For more information, see https://d.android.com/r/tools/task-configuration-avoidance.
To determine what is calling variant.getJavaCompile(), use -Pandroid.debug.obsoleteApi=true on the command line to display more information.
Affected Modules: rsdroid

Re-enable Sync methods

Syncing methods were disabled due to OpenSSL build issues on Windows

We could remove references OpenSSL and a use a rust-based component, or diagnose & patch upstream.

It was likely a path character/space in filename issue.

Implement CI: Building on PR

  • Linux Build
  • MacOS Build
  • Windows Build

Each performing:

  • 4 ABI Split (x86, ARM, with 64bit varieties)
  • Instrumented Tests

  • rsdroid-tesing Build
    • Execute native tests (macOS, Windows, Linux)
    • Generate artifact

Error on clone: fatal: remote error: upload-pack: not our ref a453ba260479fc4fb0ad45f4d131c9cdce4eb853

PS C:\GitHub> git clone --recurse-submodules -j8 https://github.com/david-allison-1/Anki-Android-Backend.git
Cloning into 'Anki-Android-Backend'...
remote: Enumerating objects: 2068, done.
remote: Counting objects: 100% (599/599), done.
remote: Compressing objects: 100% (213/213), done.
remote: Total 2068 (delta 412), reused 489 (delta 352), pack-reused 1469
Receiving objects: 100% (2068/2068), 451.00 KiB | 1.07 MiB/s, done.
Resolving deltas: 100% (1031/1031), done.
Submodule 'anki' (https://github.com/david-allison-1/anki) registered for path 'rslib-bridge/anki'
Cloning into 'C:/GitHub/Anki-Android-Backend/rslib-bridge/anki'...
remote: Enumerating objects: 31389, done.
remote: Counting objects: 100% (14/14), done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 31389 (delta 3), reused 9 (delta 1), pack-reused 31375
Receiving objects: 100% (31389/31389), 14.25 MiB | 1.56 MiB/s, done.
Resolving deltas: 100% (23548/23548), done.
fatal: remote error: upload-pack: not our ref a453ba260479fc4fb0ad45f4d131c9cdce4eb853
fatal: the remote end hung up unexpectedly
Fetched in submodule path 'rslib-bridge/anki', but it did not contain a453ba260479fc4fb0ad45f4d131c9cdce4eb853. Direct fetching of that commit failed.
PS C:\GitHub>

https://github.com/david-allison-1/Anki-Android-Backend/tree/main/rslib-bridge lists Anki at a453ba2

2206215#diff-beb01594818ce241182d931b71c77810c1498614a4d0ed2a14695ec5097e711a

This doesn't exist, but the project builds on CI?

getCount: Unable to obtain row count

Collection:deleteNotesWithWrongFieldCounts

getCount() is rarely used and requires more testing

2021-01-23 09:18:50.267 10064-12361/com.ichi2.anki E/Collection: Failed to execute integrity check
    java.lang.IllegalStateException: Unable to obtain row count
        at net.ankiweb.rsdroid.database.StreamingProtobufSQLiteCursor.getCount(StreamingProtobufSQLiteCursor.java:78)
        at com.ichi2.libanki.Collection.deleteNotesWithWrongFieldCounts(Collection.java:1814)
        at com.ichi2.libanki.Collection.lambda$fixIntegrity$3$Collection(Collection.java:1471)
        at com.ichi2.libanki.-$$Lambda$Collection$bVvDp2vCnp4nL0IP4hLyR55WilU.apply(lambda)
        at com.ichi2.libanki.Collection.lambda$fixIntegrity$1$Collection(Collection.java:1430)
        at com.ichi2.libanki.-$$Lambda$Collection$LAlIj8YurOJQPxl63-Nq4BUeZTI.consume(lambda)
        at com.ichi2.libanki.Collection.fixIntegrity(Collection.java:1471)
        at com.ichi2.async.CollectionTask$CheckDatabase.task(CollectionTask.java:1210)
        at com.ichi2.async.CollectionTask$CheckDatabase.task(CollectionTask.java:1201)
        at com.ichi2.async.CollectionTask.actualDoInBackground(CollectionTask.java:221)
        at com.ichi2.async.CollectionTask.doInBackground(CollectionTask.java:184)
        at com.ichi2.async.CollectionTask.doInBackground(CollectionTask.java:107)
        at android.os.AsyncTask$2.call(AsyncTask.java:305)
        at java.util.concurrent.FutureTask.run(FutureTask.java:237)
        at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
        at java.lang.Thread.run(Thread.java:761)

Handle database schema upgrade from v11 to v16

This is a the most involved change of the conversion.
It will be difficult to perform this in an all-or-nothing fashion because it will impact most aspects of libAnki in some manner

General Idea:

  • Upgrade anki submodule to a stable version (current commit is buggy: ankitects/anki#766)
  • Determine data persistence functionality required for schema upgrade
    • Port each behind a backwards-compatible interface if possible
  • Determine where database should be downgraded to Schema 11

Current schema version is defined in:

https://github.com/ankitects/anki/blob/af911aa66992d08278b4bca61be7e4ae5ce7c3fa/rslib/src/storage/upgrades/mod.rs

Enable local CI for ubuntu-latest using nektos/act

https://github.com/nektos/act/ should work for ubuntu

It felt like there were likely to be environment differences between the VM and a live GitHub environment (environment variables weren't defined), and I shelved this after a short while, with the plan to ensure that it works on GitHub, then spend more time with the intricacies of setting up the local runner, knowing we have a working alternative.

Tested via act -P ubuntu-latest=nektos/act-environments-ubuntu:18.04

This may be related to issue 265 on their tracker.

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.