Giter Site home page Giter Site logo

base-image's Introduction

base-image

A raspiOS-like image to use as master for both Kiwix Offspot and OLIP through the use of image-creator.

release CodeFactor Build Status License: GPL v3

Scope

This produces a raspiOS-like image that will, in a compatible device:

  • Setup a (configurable) WiFi Access Point
  • Setup ethernet networking (accoding to configuration)
  • Setup a docker-compose project

Our target is all Raspberry Pi 3 models and newer. Currently: 3A+, 3B, 3B+, 4B, 400, Zero 2 W.

Usage

Requirements: docker, python3. It is possible to build the image without docker (--no-docker) but requires specific host system and requirements (see pi-gen)

./builder.py --help

Testing Images

Releases are available from https://drive.offspot.it/base/. Test builds are uploades to S3. Links are available in the run's details.

Regular tests can be done via dockerpi

docker run -it -v $(pwd)/offspot-base.img:/sdcard/filesystem.img lukechilds/dockerpi pi3

Don't validate an image without testing on actual hardware by flashing the Image on a microSD card.

Troubleshooting

This tool mainly prepares a pi-gen custom build so checking the troubleshooting section there should be a first in case of any trouble. You should also be familiar with its README.

Using non-standard hardware

We only provide support for our fixed targets list and scenarios. That said, it's a raspiOS system so you should be able to adapt it easily.

  • Pi Compute Module matching a supported Model with WiFi should work directly.
  • No WiFi scenario is not supported. You'll need to tweak the runtime-config script so it doesn't start hostapd.
  • Using additional WiFi interfaces is possible (as client or as AP). If chipset is not supported, its driver must be installed.
  • Non-arm would require tweaking the tree and the script to use regular debian sources instead of raspbian.
  • 32b ARM (Pi 0/1/2) can work. Use the --arch=armhf flag of the builder. In the running system, remove docker and its source-list then reinstall it from raspbian repo.
apt-get remove docker-ce docker-ce-cli containerd.io docker-compose-plugin runc
rm /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install docker.io docker-compose

Please, Open a ticket explaining what you're trying to achieve and why. We may be able to provide directions.

base-image's People

Contributors

kelson42 avatar rgaudin avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

base-image's Issues

Docker purge strategy

With #31, we now have a docker system in place but there's no purge strategy.

What a common purge strategy should be, if any, is not obvious. Given we are not connected, we won't be able to pull so evicting images should not be taken lightly.

For Kiwix scenario, I believe we are fine with zero purging. We'll only add the images we need at creation time and docker-compose will create/reuse the corresponding containers.

OLIP orchestrate images and containers so the scenario is probably more complex but in this case I believe OLIP does (or would do) the cleanup.

In the end, I am suggesting to not add any automatic purging whatsoever but to agree/document that it's on purpose.

docker-images-loader should check docker

docker-images-loader doesn't check whether docker (balena) is ready of not before attempting to load images.
Because the socket is started first, load seem to start but then waits for a response forever.
This has been circumvented by making sure the script depends on balena.service and not the socket but the script should check anyway because that's otherwise hard to debug

Reduce writes to disk

In order to preserve flash card storage and keep the root partition size under control, we want to reduce the number of writes to the minimum.

This implies disabling all logs that we don't need and setting strict logrotate configurations for those we want to keep – should there be any.

This doesn't concern docker applications which are handled both at application's code level and within #6.

The option we discussed together is the one followed by @fheslouin in caff30f which consist of using tmpfs for /tmp and most of /var. Giving it a second though, it seems fragile as:

  • it consumes RAM which is a limited resource ; especially on low-end devices like Pi0 (512M)
  • limiting this effect would mean limiting writes which goes in the same direction. Better have low logs writes on SD and not using RAM.
  • important logs are generated in containers and stored on content partition (http server) anyway.

@fheslouin @letompouce, what do you think?

TINC setup

Tinc setup is currently handled by an ansible role.

Tinc binary can be simply install through package manager, the systemd unit is available in our repo.

bsf-agent container will take care of Tinc init process and unit management, however an interface has to be created on host

Remove some pigen packages

Here's the list of packages to remove, from @fheslouin:

Unfortunately, we can't remove the drivers as we need to support external WiFI dongles as Pi Zero, Pi and Pi 2 have no integrated WiFi chipset.

@fheslouin @letompouce, please confirm it's OK to include drivers in the base image and maybe remove it in your post-install step.

OLIP WebApp & ExFAT issue

We have two kind of WebApp :

  • The one that needs to manage ownership and permissions on folder mount from host device (MariaDB, NextCloud)
  • The one that needs to create symbolic link from container to mounted folder from host device (OLIP Mediacenter)

Currently OLIP use /data/ on host device and share it with all WebApp and each WebApp has is own folder within /data/.

As discussed earlier, a possibility is to create an other ext4 loop device to host /data/, we still have to discuss the way to implement it.

This issue is a follow up of #4, #5 and #28

Add data partition

We want to use a three partitions scheme with:

  • an ext4 “rootfs” (already in raspian) partition for the system
  • rootfs partition gets an extra 300Mi? from base-image disk usage
  • an exfat “data” partition for image, containers and content storage.
  • an in-file ext4 filesystem on /data/docker.fs to host the containers storage file.

exFAT allows anyone to plug the card on a computer and mount the data partition (Linux, macOS, Windows).
This allows for content updates in some cases and eases maintenance.

It also allows the image-creator to be multi-platform as its role consists of mounting data partition
to download image tarballs and content inside.

exFAT comes with a few limitations though:

  • no permissions support. filesystem is mounted with a set user, group and mask which is applied to all the files. This prevents permissions-sensitive tools from working off of it such as the containers task. That's the reason we are storing the containers files within an in-file fs itself in ext4.
  • no resize support (only on Windows to be more precise). This means we need to know the actual target SD card size when building the image. image-creator will thus require the target SD card size as input as it will resize the partition (not the FS) and recreate the filesystem before installing the content and images on it.

Upload .info to storage backend

It is important to be able to upload, for the CI/CD, the .info beside the image to ease, if needed, the investigation on a specific image.

To allow this, it would be good, if #15 is specified, that this file is moved/renamd to my_image_filename.info so I don't have to fiture out where to find this file.

Consider using podman

Podman might be a better alternative than docker-ce:

  • daemon-less
  • lighter than dockerd
  • systemd integration (for use by OLIP in its post-install step)
  • docker-compose and docker API compatible when using the podman service

What Hardware to support?

It feels kind of backward to ask this now while it should probably have been a defining point and should be written clearly on the README. Let's discuss it then.

It is understood that:

  • Core platform/target is the Raspberry Pi.
  • BSF is using Pi4 only for this project. Other non-Pi hardware wont be updated.
  • We don't want to support any generic computer (as does IIAB).
  • Kiwix hotspot has been working on all standard Pi models (Zero, 1, 2, 3, 4, 400).

Raspberry Pi has gotten quite diverse over the years. Here's a matrix of Pi models that are of interest to us (so no Pico nor Compute Module).

Given the service we provide, two characteristics are of interest to be discussed:

  • CPU architecture
  • WiFi capability

WiFi capability

  • Do we want to support models without integrated Wireless?
  • If so, to what extent ? Do we want to provide a list of WiFi chipsets (or makers) we'd support?

It is understood that we want, at some point, to clearly state the performance capabilities of the solution (“supports users on a Pi 4”) which would not be possible with those.

We could still support other chipset in a “best effort” manner: try it and let us know if that works. That's kind of the current state as we do include (specifically we don't remove) a number of WiFi chipsets and should those support AP mode and the options we use (very limited at the moment offspot/offspot-config#2), it should work.

Impact: we currently don't support no WiFi so we'd need to at least introduce a graceful fallback.

CPU architecture

Pi models can be split over three different architectures:

  • armv6, also called armel is 32b for Pi Zero and Pi 1. Those were released in 2012-2014 and feature at most 512MB RAM
  • armv7 also called armhf is 32b for Pi 2 only, released in 2015 with 1GB RAM.
  • armv8 also called aarch64 or arm64 is 64b and powers all the other (newer) models.

Important note

  • armhf means armv7 in Debian lingo but armhf means armv6+vfp2 in raspbian one (All armv6 Pi have a VFPv2).
  • A Debian system running an armv7l Kernel with armhf dpkg architecture is thus different from a Raspbian (or base-image) running an armv6l kernel on an armhf dpkg-architecture.
  • Debian supports armv6 but calls it armel in dpkg.
  • ⚠️ adding debian source list must be done carefully, forcing the architecture.

https://eloydegen.com/blog/posts/arm-linux-distribtions/

RaspiOS support

Obviously, raspiOS supports all three platforms. Its armhf image includes four kernels (armv6-32, armv7-32, armv8-32 and armv8-64) but the binaries are all armv6-compatible.

RaspiOS also offers a second image named arm64 with binaries for armv8.

We currently offer similar choice.

If RaspiOS does it and we're based off raspiOS, why are we even discussing this?

Drawbacks of keeping armv6 and armv7

armv6 and armv7 have very different statuses and support and the latter is a lot more common but regarding our table above, keeping it would be only to support Pi2 which was release in 2015 without integrated WiFi.

Docker

  • Docker works on armv6 via raspbian and/or debian package but included version is close to 2yo. Not much of a problem now but sill limiting.
  • Docker has a repository with latest version that is the recommended way to install but this works only for armhf (debian) so it doesn't work on armv6.
  • Docker has static builds for armv6 but those are broken and doesn't work.
  • It's almost impossible to test armv6 image as docker uses qemu for that and we don't control exactly how it's called.
  • Docker image for library/debian (base image for most language-specific images) doesn't even provide an arm/v6 variant but an arm/v5 variant. Not sure how the (undocumented) behavior is when requesting v6.
$ docker run --platform linux/arm/v5 -it debian bash -c 'uname -m && dpkg --print-architecture && file /lib/arm-linux-gnueabi/ld-2.31.so
armv7l
armel
/lib/arm-linux-gnueabi/ld-2.31.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, BuildID[sha1]=261b75978776d17373d9f528db8cd5d50ecf528f, stripped

$ docker run --platform linux/arm/v6 -it debian bash -c 'uname -m && dpkg --print-architecture && file /lib/arm-linux-gnueabi/ld-2.31.so
armv7l
armel
/lib/arm-linux-gnueabi/ld-2.31.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, BuildID[sha1]=261b75978776d17373d9f528db8cd5d50ecf528f, stripped

$ docker run --platform linux/arm/v7 -it debian bash -c 'uname -m && dpkg --print-architecture && file /lib/arm-linux-gnueabi/ld-2.31.so
armv7l
armel
/lib/arm-linux-gnueabi/ld-2.31.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, BuildID[sha1]=261b75978776d17373d9f528db8cd5d50ecf528f, stripped

Node

Python

  • Most python libraries are pure-python and don't matter the architecture (python works on all of them)
  • A few important ones do matter (like Pillow which is ultra-dominant for image processing)
  • Important projects do provide binary packages (wheels) but those are specific to a platform (interpreter, system, architecture)
  • Architecture is taken from kernel so in-docker (or docker buildx to be more specific), it's either seen as aarch64 or armv7l. This happens both when finding package and when building package. Given not many people care about arm 32b, it may lead either to improper package or not finding the package.
  • Not found package means building from source which is very very long in qemu-arm and requires compilation tools, and libraries and all (if it's working)

In conclusion, armv6 support has lost most interest for people and has thus became exotic. armv7 has better support but is still 32b and supporting it wouldn't make much sense.

I suggest we agree on dropping support for armv6 and armv7 and thus only publish an arm64 image that will work on all the newer Pi which are all ARMv8 (including the Pi Zero 2 W).

This will keep us (for now) in a hack-free, up-to-date platform that we can safely build-upon. Images will be easier to build and maintain as well.

Release 1.0.0

In an attempt to have something released, and move a bit forward, I would like to propose to release a version 0.1

Filesystem not properly resized on boot

Base-image's offspot_bi_init_resize.sh is properly executed on first boot and the UI shows that “Resizing” and even a resize2fs command but filesystem's size is still identical to image's one

root@yala:~# fdisk -l /dev/mmcblk0
Disk /dev/mmcblk0: 29.72 GiB, 31914983424 bytes, 62333952 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xb690ddda

Device         Boot   Start      End  Sectors  Size Id Type
/dev/mmcblk0p1         8192  1056767  1048576  512M  c W95 FAT32 (LBA)
/dev/mmcblk0p2      1056768  6209535  5152768  2.5G 83 Linux
/dev/mmcblk0p3      6209536 62333951 56124416 26.8G  7 HPFS/NTFS/exFAT
root@yala:~# df -h /data/
Filesystem      Size  Used Avail Use% Mounted on
/dev/mmcblk0p3  891M  692M  149M  83% /data

Change WiFi firmware to support more clients

Stock WiFi firmwares support too few number of clients for use in our Hotspot.
We need to use specific firmware versions with optimized number of clients.
Those are available in IIAB's repo

Publish digests for files

For easy checksum verification, we should compute digests for the image and the compressed file into files and upload them along with the actual files so image-creator can find them and verify the base image as well

Better workflow run names

When launched manually, we don't have access to branch name and the workflow run is the name of the workflow.

Crash of builder just after extracting packages

I: Extracting mount...
I: Extracting ncurses-base...
I: Extracting ncurses-bin...
I: Extracting passwd...
I: Extracting perl-base...
I: Extracting sed...
I: Extracting sysvinit-utils...
I: Extracting tar...
I: Extracting tzdata...
I: Extracting util-linux...
I: Extracting zlib1g...
W: Failure trying to run: chroot "/pi-gen/work/offspot-base/stage0/rootfs" /bin/true
W: See /pi-gen/work/offspot-base/stage0/rootfs/debootstrap/debootstrap.log for details
rmdir: failed to remove '/pi-gen/work/offspot-base/stage0/rootfs/debootstrap': Directory not empty
[14:02:41] bootstrap failed: please check /pi-gen/work/offspot-base/stage0/debootstrap.log

real	6m1.728s
user	0m0.129s
sys	0m0.146s
DEBUG:builder:Removing existing container pigen_work
pigen_work

Create builder script

A builder script would allow us to dynamically:

  • download a copy of pigen
  • customize the basic config
  • apply our modifications without maintaining a fork
  • build the image

Add option to specify image name name

For the CI/CD it would be better to specify directly the .img filename of the image name. This request to simplify afterward the finding of the file and its upload to its finale storage backend.

Specifying something like --image-path=/tmp/my_image.img would be good.

tempdir not properly detected if build-dir is not specified

$ ./builder.py 
WARNING:builder:build-dir exists. reusing (/tmp/pigenm9cvdnce
INFO:builder:Writing config
INFO:builder:Updating packages list
ERROR:builder:FAILED. An error occurred: [Errno 2] No such file or directory: '/tmp/pigenm9cvdnce/stage2/01-sys-tweaks/00-packages'
ERROR:builder:[Errno 2] No such file or directory: '/tmp/pigenm9cvdnce/stage2/01-sys-tweaks/00-packages'
Traceback (most recent call last):
  File "/home/kelson/code/base-image/./builder.py", line 247, in main
    sys.exit(builder.run())
  File "/home/kelson/code/base-image/./builder.py", line 82, in run
    self.update_packages()
  File "/home/kelson/code/base-image/./builder.py", line 137, in update_packages
    with open(fpath, "r") as fh:
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/pigenm9cvdnce/stage2/01-sys-tweaks/00-packages'

Upload uncompressed base image

Base image is used always and thus extracted always from cache. For sake of Cache's agnosticism, it'd be good to offer an uncompressed base image online as well and use it for our cache-backed workers

Display connection instructions on tty1

Uninformed users might try to connect the Host Pi to a monitor for use instead of using the WiFi/eth connection.
We should display a message leading them to the WiFi in the first tty.

Ideally, displaying the AP's SSID (and passphrase?), whether it allows Ethernet connection would be even better but given we don't store those information, that would be significantly more work and code (inspection).

We don't need it at the moment. A static message would be sufficient for now. If the need for more arise, we'll reconsider.

From offspot/kiwix-hotspot#683

System Configurator

System configurator is the script(s) that configures a running system upon startup. It is responsible for many features and was initially described in https://github.com/offspot/image-creator#json-configurator

It is not 100% clear where this tool should be developed, stored in the image and at which stage (base-image, image-creator) it should be added/updated if not both.

For the moment, let's have it in-here, store it in /usr/local/bin and add it to the base-image. Time and experience will tell if this is appropriate or not.

Initial features:

  • hostname
  • timezone
  • network
  • wireless AP
  • dns
  • dhcp server

Drop exfat support

Following #34, we've decided to not use exFAT for the third partition. We thus need to undo what has been done in this direction. Mostly:

  • filesystem of third partition. We'll keep a third, /data partition.
  • virtual.fs loop device/file. We'll keep containerd and docker data on third partition though.
  • exfatutils
  • exfat module

Reduce memory footprint

Based on #45, we target the following devices:

  • Pi 3A+ (512MB)
  • Pi 3B (1GB)
  • Pi 3B+ (1GB)
  • Pi 4B (1GB, 2GB, 4GB, 8GB)
  • Pi 400 (4GB)
  • Pi Zero 2W (512MB)

Of this list, 2 devices have 512MB RAM: Pi 3A+ (arguably old) and Zero 2W which is a recent device that respond to specific use cases.

An off the shelf base-image currently leaves only 128MB free RAM for all applications. That's not enough to support our baseline of tools (captive-portal, home, metrics) and leave enough for content apps. Reminder: we're using Docker so a lot of stuff are duplicated.

A Quick look at htop shows a great deal of RAM is used by thttpd so maybe that's the main culprit here; although that's strange since its job is so limited. Given it's only used in the base-image to serve as a demo, we can't really count it as base-image usage.

More investigation is needed with actual data.

Disable swap

Choosing whether to use swap or not on our images is important as it defines how we want to handle lack of RAM resources mostly.

Allowing swap means managing swap at host and containers level which adds complexity.

We also agree that noswap provides the most reliable and easy-to-recover experience in that containers will be killed.

Make hotspot directly pluggable onto screen

In GitLab by @Popolechien on Nov 1, 2019, 10:31

So we have a use case where everyone in the room has a tablet or smartphone but the teacher also wants to showcase on a large screen (projector or TV screen). Ideally, we could/should be able to simply plug the hotspot onto it and have visual display.

Build image in Github Actions

Base image must be built inside an Actions workflow:

In addition to building the image, it should be uploaded, zipped:

  • onto some stable location for tagged releases
  • into another location (tmp.kiwix.org/ci or some S3 bucket) for named branches (manually triggered)

@kelson42 @fheslouin @letompouce we need to clarify destinations

incorrect reporting of docker-images-loader error

docker-images-loader seems to ignore errors ; see output on a corrupt fs

user@offspot-base:~ $ sudo /usr/sbin/docker-images-loader.py
INFO: Looking for docker images in /data/images
INFO: ghcr.io_offspot_dashboard.tar FOUND
ERROR: >> Unable to identify as valid tar image ("filename 'manifest.json' not found"). Skipping.
ERROR: "filename 'manifest.json' not found"
Traceback (most recent call last):
  File "/usr/sbin/docker-images-loader.py", line 20, in <module>
    manifest = json.loads(tar.extractfile("manifest.json").read())
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/tarfile.py", line 2127, in extractfile
    tarinfo = self.getmember(member)
              ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/tarfile.py", line 1813, in getmember
    raise KeyError("filename %r not found" % name)
KeyError: "filename 'manifest.json' not found"
INFO: ghcr.io_offspot_file-browser.tar FOUND
INFO: >> Loading ghcr.io/offspot/file-browser:1.0…
5d3e392a13a0: Loading layer [==================================================>]  5.573MB/5.573MB
7b7e69432069: Loading layer [==================================================>]  838.1kB/838.1kB
e280419bea10: Loading layer [==================================================>]  19.97kB/19.97kB
22e96bdc9a57: Loading layer [==================================================>]     39MB/39MB
invalid diffID for layer 3: expected "sha256:22e96bdc9a57938bd6487bfe3d8d95c791fe270e50c615b9923d4346797e72a2", got "sha256:b5a47d3499b87963d58b6c19748e477c371ccd7e37c02f26e59479a3443801f9"
ERROR: Unable to load ghcr.io/offspot/file-browser:1.0 into docker
INFO: ghcr.io_offspot_reverse-proxy.tar FOUND
ERROR: >> Unable to identify as valid tar image ("filename 'manifest.json' not found"). Skipping.
ERROR: "filename 'manifest.json' not found"
Traceback (most recent call last):
  File "/usr/sbin/docker-images-loader.py", line 20, in <module>
    manifest = json.loads(tar.extractfile("manifest.json").read())
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/tarfile.py", line 2127, in extractfile
    tarinfo = self.getmember(member)
              ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/tarfile.py", line 1813, in getmember
    raise KeyError("filename %r not found" % name)
KeyError: "filename 'manifest.json' not found"
INFO: ghcr.io_offspot_captive-portal.tar FOUND
ERROR: >> Unable to identify as valid tar image ("filename 'manifest.json' not found"). Skipping.
ERROR: "filename 'manifest.json' not found"
Traceback (most recent call last):
  File "/usr/sbin/docker-images-loader.py", line 20, in <module>
    manifest = json.loads(tar.extractfile("manifest.json").read())
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/tarfile.py", line 2127, in extractfile
    tarinfo = self.getmember(member)
              ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/tarfile.py", line 1813, in getmember
    raise KeyError("filename %r not found" % name)
KeyError: "filename 'manifest.json' not found"
user@offspot-base:~ $ echo $?
0

Add 64b support

Basic support for 64b is already present in the build script (using --64b).

We need to build a 64b image in the CI to ensure changes are not breaking or arch or the other (64b is based on a different branch of pi-gen)

We also need to publish the 64b image as well.

  • Update script to a more explicit naming: --arch armhf|arm64
  • Update build workflow to run both

PiSugar Power Manager setup

PiSugar Power Manager is the software that manage the Pi Sugar 3 battery manager.

3 packages needs to be installed in unattended mode :

The systemd unit for the Raspberry Pi Red LED management is a nice to have, but really BSF and Pi Sugar specific, we might have to drop this feature...to be discuss

Update to latest pi-gen

raspiOS has been updated to 2023-10-10-raspios-bookworm and 2023-10-10-raspios-bookworm-arm64

Add minimum maintenance tools

For development, debug and maintenance, it is occasionally required to log-into the Pi. To ease with the common tasks associated with such operations, the following packages, installed on the host, would help

  • vim-tiny ~2.5M (micro is way larger)
  • screen ~1M

FYI, bash-completion, git, curl, wget, unzip, zip, p7zip-full are already part of the distrib.

Add container stack

We need the base image to have a running container stack with a working docker-compose setup.

We should also have a container-loading mechanism that looks for images tarball in /data/images and load them into the container stack on startup, before docker-compose is started.

We also need to generate the docker-compose.yaml file from a script using the list of apps in /boot/project.json

  • Container Stack
  • Images-loading script
  • Docker compose setup
  • Docker-compose.yaml generating script from project.json
  • Default project.json include a very lightweight webserver that solely returns said config file
  • Properly ordered units so docker-compose starts after images are loaded and yaml has been generated

Images loading pseudo-code

if not images_root.has_files():
   exit()  # continues with docker-compose gen
for file in images_root.all():
    docker.load(file)
    file.unlink()

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.