This commit is contained in:
Ashe Connor 2019-01-14 13:28:53 +11:00
Родитель 95fe613b11
Коммит 286996b52c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 997E657B1C52B6C5
18 изменённых файлов: 685 добавлений и 8 удалений

3
.gitmodules поставляемый Normal file
Просмотреть файл

@ -0,0 +1,3 @@
[submodule "docker/VFSForGit"]
path = docker/VFSForGit
url = https://github.com/github/VFSForGit.git

7
.travis.yml Normal file
Просмотреть файл

@ -0,0 +1,7 @@
language: ruby
services:
- docker
script:
- docker/cibuild

21
docker/Dockerfile-develop Normal file
Просмотреть файл

@ -0,0 +1,21 @@
FROM github/fuse3-linux
LABEL org.label-schema.name="projfs-develop"
LABEL org.label-schema.description="projfs Linux libprojfs development image"
LABEL org.label-schema.vendor="GitHub"
LABEL org.label-schema.schema-version="1.0"
WORKDIR /data/projfs
RUN set -ex && \
apt-get update -qq && \
BUILD_DEPS=' \
automake \
build-essential \
libtool \
' && \
apt-get install -y -qq --no-install-recommends $BUILD_DEPS && \
rm -rf /var/lib/apt/lists/*
ARG UID
RUN useradd -d /data -M -s /bin/bash -u $UID -G 0 user

58
docker/Dockerfile-distpkg Normal file
Просмотреть файл

@ -0,0 +1,58 @@
FROM github/fuse3-linux
LABEL org.label-schema.name="projfs-distpkg"
LABEL org.label-schema.description="projfs Linux libprojfs distribution image"
LABEL org.label-schema.vendor="GitHub"
LABEL org.label-schema.schema-version="1.0"
ARG PROJFS_REPO=projfs-linux-private
ENV BUILD_DEPS ' \
automake \
build-essential \
dpkg-dev \
libtool \
pkg-config \
'
RUN apt-get update -qq && \
apt-get install -y -qq --no-install-recommends $BUILD_DEPS && \
rm -rf /var/lib/apt/lists/*
COPY repos/$PROJFS_REPO /tmp/$PROJFS_REPO
WORKDIR /tmp/$PROJFS_REPO
RUN ./autogen.sh
RUN make -j "$(nproc)" distclean
RUN ./configure --enable-vfs-api
RUN make -j "$(nproc)" dist
WORKDIR /tmp
RUN \
PROJFS_VERSION=$(pkg-config --modversion $PROJFS_REPO/projfs.pc) && \
PROJFS_RELEASE="libprojfs-$PROJFS_VERSION" && \
PROJFS_DISTPKG="$PROJFS_RELEASE.tar.xz" && \
\
tar -xJf "$PROJFS_REPO/$PROJFS_DISTPKG" && \
rm -rf "$PROJFS_REPO" && \
mv "$PROJFS_RELEASE" libprojfs-release
WORKDIR /tmp/libprojfs-release
RUN \
DEB_HOST_MULTIARCH="$(dpkg-architecture --query DEB_HOST_MULTIARCH)" && \
./configure --prefix=/usr \
--libdir="\${prefix}/lib/$DEB_HOST_MULTIARCH" \
--libexecdir="\${prefix}/lib/$DEB_HOST_MULTIARCH" \
--sysconfdir=/etc \
--disable-static \
--enable-shared \
--enable-vfs-api
RUN make -j "$(nproc)" install
WORKDIR /tmp
RUN \
DEB_HOST_MULTIARCH="$(dpkg-architecture --query DEB_HOST_MULTIARCH)" && \
rm -rf /tmp/libprojfs-release && \
rm -f /usr/lib/$DEB_HOST_MULTIARCH/libprojfs.la
RUN apt-get purge -y --auto-remove $BUILD_DEPS

36
docker/Dockerfile-fuse3 Normal file
Просмотреть файл

@ -0,0 +1,36 @@
FROM debian:stretch
LABEL org.label-schema.name="projfs-fuse3"
LABEL org.label-schema.description="projfs Linux libfuse v3 image"
LABEL org.label-schema.vendor="GitHub"
LABEL org.label-schema.schema-version="1.0"
ARG FUSE_VERSION=3.3.0
ARG FUSE_SHA256=c554863405477cd138c38944be9cdc3781a51d78c369ab878083feb256111b65
WORKDIR /tmp
# fuse3 requires meson 0.42, available from stretch-backports
# wget depends on ca-certificates
RUN \
echo 'deb http://deb.debian.org/debian stretch-backports main' >> /etc/apt/sources.list.d/stretch-backports.list && \
apt-get update -qq && \
apt-get install -y -qq --no-install-recommends build-essential ca-certificates pkg-config wget udev && \
apt-get install -y -qq --no-install-recommends -t=stretch-backports meson && \
rm -rf /var/lib/apt/lists/*
ENV FUSE_RELEASE "fuse-$FUSE_VERSION"
ENV FUSE_DOWNLOAD "$FUSE_RELEASE.tar.xz"
ENV FUSE_URL_PATH "releases/download/$FUSE_RELEASE/$FUSE_DOWNLOAD"
RUN \
wget -q "https://github.com/libfuse/libfuse/$FUSE_URL_PATH" && \
echo "$FUSE_SHA256 $FUSE_DOWNLOAD" | sha256sum -c -
RUN tar -xJf "$FUSE_DOWNLOAD"
WORKDIR "$FUSE_RELEASE/build"
RUN meson --prefix=/usr --sysconfdir=/etc --localstatedir=/var ..
RUN ninja -j "$(nproc)"
RUN ninja install

Просмотреть файл

@ -0,0 +1,19 @@
FROM microsoft/dotnet:2.2-sdk-stretch
ARG FUSE_VERSION=3.3.0
WORKDIR /data/integrate
ENV HOME /data/integrate
COPY --from=github/fuse3-linux \
/usr/lib/x86_64-linux-gnu/libfuse3.so.$FUSE_VERSION \
/usr/lib/x86_64-linux-gnu/
RUN set -ex && \
cd /usr/lib/x86_64-linux-gnu && \
ln -s libfuse3.so.$FUSE_VERSION libfuse3.so.3 && \
ln -s libfuse3.so.3 libfuse3.so
# Allow VFSforGit to load locally-compiled libprojfs binaries for development
ENV LD_LIBRARY_PATH /data/projfs/lib/.libs

5
docker/Dockerfile-vfs Normal file
Просмотреть файл

@ -0,0 +1,5 @@
FROM microsoft/dotnet:2.2-sdk-stretch
WORKDIR /data/vfs/src
ENV HOME /data
RUN chmod 0777 /data

136
docker/README.md Normal file
Просмотреть файл

@ -0,0 +1,136 @@
# projfs-docker
Build projfs with Docker.
- [Getting started](#getting-started)
- [How it works](#how-it-works)
## Getting started
``` console
$ ./projfs
usage: ./projfs [command]
command:
setup
Build all containers and run all build scripts necessary for
development work (excludes distpkg).
image fuse3|develop|distpkg|vfs|integrate
Build the Docker build environment for the specified component.
develop configure|make|test|dist|clean
Builds libprojfs for local development and testing.
configure: runs autogen with appropriate switches
make: runs make
test: runs make test
dist: runs make dist
clean: runs make clean
vfs restore|make
Build VFSForGit.
restore: do a NuGet restore
make: build everything
integrate clone|mount
Run the integration.
clone: run the MirrorProvider clone
mount: mount the MirrorProvider
run IMAGE [OPTION ...] -- CMD [ARG ...]
Run a command in the specified image (`fuse3', `develop', `vfs', etc.)
Everything up until `--' is passed as options to `docker run', i.e.
before the image name is given. If no options are specified, defaults
for the image will be used (e.g. enables FUSE for the 'integrate'
image).
exec IMAGE [OPTION ...] -- CMD [ARG ...]
Run a command in a container already started with `./projfs run'.
test [--force]
Run the test suite. Will fail if there's already a running integration
container, unless --force is specified, in which case the running
container is stopped before tests are run.
```
Here's how to get started:
``` shell
# Build all images, and run all build scripts. After this finishes, build
# artifacts will be present in your local tree.
./projfs setup
# Clone the test repository (found at build/PathToMirror). The created
# repository will be at build/integrate/TestRoot.
./projfs integrate clone
# Run the built-in end-to-end test suite (will clean up after itself if
# successful).
./projfs test
# Mount the test root.
./projfs integrate mount
# In another terminal, interact with the created mount.
./projfs exec integrate -- touch TestRoot/src/xyz
./projfs exec integrate -- rm TestRoot/src/xyz
# Start a shell to play more. (pass `-t' to `docker exec')
./projfs exec integrate -t -- bash
```
## How it works
The command `./projfs image COMPONENT` will build one of five tagged Docker images.
- `./projfs image fuse3` creates `github/fuse3-linux` from `Dockerfile-fuse3`. It installs development tools and build
prerequisites in a stock Debian stretch image, then fetches and builds FUSE 3.3.0.
- `./projfs image develop` creates `github/projfs-dev-linux` from `Dockerfile-develop`. It is based on
`github/fuse3-linux`, and installs only the build tools necessary for building libprojfs.
- `./projfs image distpkg` creates `github/projfs-dist-linux` from `Dockerfile-distpkg`. It is based on
`github/fuse3-linux`. The Dockerfile builds the dist package for libprojfs, then installs it. The resulting image
represents what a clean system with the libprojfs dist package installed should look like.
- `./projfs image vfs` creates `github/vfs-linux` from `Dockerfile-vfs`. Right now this image is a plain
Microsoft-supplied .NET SDK image with light filesystem modifications for testing.
- `./projfs image integrate` creates `github/projfs-vfs-linux` from `Dockerfile-integrate`. It uses the .NET SDK image
as base, then copies the built FUSE objects from `github/fuse3-linux` and sets up the environment so libraries from
the ProjFS build (when mounted) will be located correctly.
Note that each image is only a build environment, and does not contain any source code. Each component has a range of
subcommands which will run commands in containers from each of these images; when doing so, `./projfs` mounts
directories on the Docker host (your local filesystem) into the running containers, so that up-to-date source is seen
and build artifacts are preserved.
The `projfs` component will mount the root of this repository at `/data/projfs` in the container. `./projfs develop
configure` will run `/data/projfs/autogen.sh` in the container, with the output files being placed on the Docker host.
`./projfs develop make` will then run `make`, with all build artifacts again placed on the Docker host. (You'll have a
bunch of ELF objects which you won't be able to do anything with on macOS.)
The `vfs` component mounts the source at `/data/vfs/src`, and additional directories in `build` so that NuGet restored
packages and all build output are preserved on the Docker host.
Finally, the `integrate` component mounts all of the above plus one more (the destination of the MirrorProvider clone
operation), in locations such that everything Just Works™. Build artifacts from each of the previous steps are seen in
the integration container, and paths and environment variables are set appropriately for the whole stack to function.
Note the filesystem layering that occurs when actually running the integration:
- `./projfs integrate clone` runs the MirrorProvider clone script, cloning `/data/PathToMirror` (`build/PathToMirror`)
to `/data/integrate/TestRoot` (`build/integrate/TestRoot`). The source and destination of the clone are different
Docker-mounted paths, i.e. both are on your local disk.
- `./projfs integrate mount` runs the MirrorProvider mount script on `/data/integrate/TestRoot`. `/data/integrate` is
a mount from the host path `build/integrate` in this repo, so the `TestRoot` scaffolding created by the clone
operation still exists. We then mount `/data/integrate/TestRoot/src`. This mount happens in the Docker container,
meaning the container itself accessing `/data/integrate/TestRoot/src` will interact with FUSE. Meanwhile,
`build/integrate/TestRoot/src` will appear as an empty directory on the Docker host.
We do this to allow rapid iteration without having to rebuild entire containers every time, which is required if the
entire container build process also builds the project.

1
docker/VFSForGit Submodule

@ -0,0 +1 @@
Subproject commit bc71f5de7b363c0b56b30be29e79ed1b9949b697

2
docker/build/BuildOutput/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,2 @@
*
!.gitignore

2
docker/build/integrate/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,2 @@
*
!.gitignore

2
docker/build/packages/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,2 @@
*
!.gitignore

20
docker/cibuild Executable file
Просмотреть файл

@ -0,0 +1,20 @@
#!/bin/bash
set -ex
cd "$(dirname "$0")"
./projfs image fuse3
./projfs image develop
./projfs develop configure
./projfs develop make
./projfs develop test
./projfs image vfs
./projfs vfs restore
./projfs vfs make
./projfs image integrate
./projfs integrate clone
./projfs test --force

80
docker/project.rb Normal file
Просмотреть файл

@ -0,0 +1,80 @@
# frozen_string_literal: true
class Project
def initialize(name, dockerfile:, image:, mounts: [], commands: {}, run_as_root: false, build_options: [], options: nil)
@name = name
@dockerfile = File.join(File.expand_path(File.dirname(__FILE__)), dockerfile)
@image = image
@mounts = mounts
@commands = commands
@run_as_root = run_as_root
@build_options = build_options
@options = options
end
def build(quiet: true)
raise "cannot build #@name" if ![@dockerfile, @image].all?
system("docker", "build", *(quiet ? ["-q"] : []), *@build_options, "-f", @dockerfile, "-t", @image, File.dirname(__FILE__))
end
def command(name, popen: false)
raise "cannot run command for #@name" if !@image
commands = @commands[name.intern] or raise "command #{name} not found for #@name"
commands = [commands] if commands[0].is_a?(String)
if popen
raise "cannot popen multiple commands" if commands.length > 1
run("--", *commands[0], popen: true)
else
commands.each do |command|
run("--", *command)
end
end
end
def run(*cmd, popen: false)
raise "cannot run #@name" if !@image
argv = []
argv.concat(@options) if @options && (cmd.length == 0 || cmd[0] == '--')
while cmd.any? && cmd[0] != '--'
argv << cmd.shift
end
@mounts.each { |m| argv << "-v" << m }
argv << @image
cmd.shift if cmd[0] == '--'
argv.concat(cmd)
args = ["docker", "run", *user_args, "--pid=host", "--rm", "--name", docker_container_name, *argv]
if popen
IO.popen(args, "r+", err: [:child, :out])
else
system(*args)
end
end
def exec(*cmd)
raise "cannot exec #@name" if !@image
argv = []
while cmd.any? && cmd[0] != '--'
argv << cmd.shift
end
argv << docker_container_name
cmd.shift if cmd[0] == '--'
argv.concat(cmd)
system("docker", "exec", *user_args, "-i", *argv)
end
def docker_container_name
"projfs-#@name"
end
def user_args
if @run_as_root
[]
else
["-u", Process.uid.to_s]
end
end
end
# vim: set sw=2 et:

201
docker/projfs Executable file
Просмотреть файл

@ -0,0 +1,201 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative 'project'
require_relative 'tests'
Dir.chdir(File.dirname(__FILE__))
def usage
STDERR.puts <<~USAGE
usage: #$0 [command]
command:
setup
Build all containers and run all build scripts necessary for
development work (excludes distpkg).
image fuse3|develop|distpkg|vfs|integrate
Build the Docker build environment for the specified component.
develop configure|make|test|dist|clean
Builds libprojfs for local development and testing.
configure: runs autogen with appropriate switches
make: runs make
test: runs make test
dist: runs make dist
clean: runs make clean
vfs restore|make
Build VFSForGit.
restore: do a NuGet restore
make: build everything
integrate clone|mount
Run the integration.
clone: run the MirrorProvider clone
mount: mount the MirrorProvider
run IMAGE [OPTION ...] -- CMD [ARG ...]
Run a command in the specified image (`fuse3', `develop', `vfs', etc.)
Everything up until `--' is passed as options to `docker run', i.e.
before the image name is given. If no options are specified, defaults
for the image will be used (e.g. enables FUSE for the 'integrate'
image).
exec IMAGE [OPTION ...] -- CMD [ARG ...]
Run a command in a container already started with `#$0 run'.
test [--force]
Run the test suite. Will fail if there's already a running integration
container, unless --force is specified, in which case the running
container is stopped before tests are run.
USAGE
exit 1
end
def system(*args)
puts ">> #{args.join " "}"
result = Kernel.system(*args)
if !result
raise "failed to run '#{args.join " "}"
end
end
usage if ARGV.length == 0
fuse3 = Project.new('fuse3',
dockerfile: 'Dockerfile-fuse3',
image: 'github/fuse3-linux'
)
develop = Project.new('develop',
dockerfile: 'Dockerfile-develop',
image: 'github/projfs-dev-linux',
mounts: ["#{Dir.pwd}/..:/data/projfs"],
commands: {
configure: ['./autogen.sh', '--enable-vfs-api'],
make: ['make'],
test: ['make', 'test'],
dist: ['make', 'dist'],
clean: ['make', 'clean'],
},
build_options: ['--build-arg', "UID=#{Process.uid}"],
options: ['--device', '/dev/fuse', '--cap-add', 'SYS_ADMIN'])
distpkg = Project.new('distpkg',
dockerfile: 'Dockerfile-distpkg',
image: 'github/projfs-dist-linux',
mounts: ["#{Dir.pwd}/..:/data/projfs"]
)
vfs = Project.new('vfs',
dockerfile: 'Dockerfile-vfs',
image: 'github/vfs-linux',
mounts: [
"#{Dir.pwd}/VFSForGit:/data/vfs/src",
"#{Dir.pwd}/build/packages:/data/vfs/packages",
"#{Dir.pwd}/build/BuildOutput:/data/vfs/BuildOutput",
],
commands: {
restore: [
["dotnet", "restore", "MirrorProvider/MirrorProvider.sln", "/p:Configuration=Debug.Linux", "--packages", "../packages"],
["dotnet", "restore", "ProjFS.Linux/PrjFSLib.Linux.Managed/PrjFSLib.Linux.Managed.csproj", "/p:Configuration=Debug", "/p:Platform=x64", "--packages", "../packages"],
],
make: [
["dotnet", "build", "ProjFS.Linux/PrjFSLib.Linux.Managed/PrjFSLib.Linux.Managed.csproj", "/p:Configuration=Debug", "/p:Platform=x64", "--no-restore"],
["dotnet", "build", "MirrorProvider/MirrorProvider.sln", "/p:Configuration=Debug.Linux", "--no-restore"],
]
})
integrate = Project.new('integrate',
dockerfile: 'Dockerfile-integrate',
image: 'github/projfs-vfs-linux',
mounts: [
"#{Dir.pwd}/..:/data/projfs",
"#{Dir.pwd}/VFSForGit:/data/vfs/src",
"#{Dir.pwd}/build/packages:/data/vfs/packages",
"#{Dir.pwd}/build/BuildOutput:/data/vfs/BuildOutput",
"#{Dir.pwd}/build/PathToMirror:/data/PathToMirror",
"#{Dir.pwd}/build/integrate:/data/integrate"
],
commands: {
clone: [["env", "PATH_TO_MIRROR=/data/PathToMirror", "TEST_ROOT=/data/integrate/TestRoot", "/data/vfs/src/MirrorProvider/Scripts/Linux/MirrorProvider_Clone.sh"]],
mount: [["env", "TEST_ROOT=/data/integrate/TestRoot", "/data/vfs/src/MirrorProvider/Scripts/Linux/MirrorProvider_Mount.sh"]],
},
options: ['--device', '/dev/fuse', '--cap-add', 'SYS_ADMIN', '-i'],
run_as_root: true)
images = {
"fuse3" => fuse3,
"develop" => develop,
"distpkg" => distpkg,
"vfs" => vfs,
"integrate" => integrate,
}
case ARGV.shift
when "setup"
usage if ARGV.length > 0
fuse3.build(quiet:false)
develop.build(quiet: false)
develop.command "configure"
develop.command "make"
develop.command "test"
vfs.build(quiet: false)
vfs.command "restore"
vfs.command "make"
integrate.build(quiet: false)
when "image"
usage if ARGV.length != 1
images[ARGV[0]].build(quiet: false)
when "develop"
usage if ARGV.length != 1
develop.command(ARGV[0])
when "vfs"
usage if ARGV.length != 1
vfs.command(ARGV[0])
when "integrate"
usage if ARGV.length != 1
integrate.command(ARGV[0])
when "run"
index = ARGV.index("--")
usage if ARGV.length < 3 || index == nil || index == ARGV.length - 1
image = images[ARGV[0]] || usage
image.run(*ARGV[1..-1])
when "exec"
index = ARGV.index("--")
usage if ARGV.length < 3 || index == nil || index == ARGV.length - 1
image = images[ARGV[0]] || usage
image.exec(*ARGV[1..-1])
when "test"
force = false
while arg = ARGV.shift
case arg
when "--force" then force = true
else usage
end
end
tests(images, force: force)
else
usage
end
# vim: set sw=2 et:

74
docker/tests.rb Normal file
Просмотреть файл

@ -0,0 +1,74 @@
# frozen_string_literal: true
def report(name, text)
text.split("\n").each do |line|
puts "#{name}: #{line}"
end
end
def wait_for(name, io, msgs, timeout: 1)
buf = String.new
start = Time.now
while !(msg = msgs.find {|m| buf.include?(m)})
begin
s = io.read_nonblock(1024)
rescue IO::WaitReadable
remaining = timeout - (Time.now - start)
raise "timed out waiting for #{msg.inspect}" if remaining <= 0
any = IO.select([io], nil, nil, remaining)
retry if any
raise "timed out waiting for one of #{msgs.inspect}"
rescue EOFError
raise "timed out waiting for one of #{msgs.inspect} (got EOF)"
end
report(name, s)
buf << s
end
msg
end
MSG_PRESS_ENTER = "Press Enter to end"
MSG_CONFLICT = "Conflict. The container name"
def tests(images, force: false)
integration = images["integrate"].command("mount", popen: true)
msg = wait_for("integrate", integration, [MSG_PRESS_ENTER, MSG_CONFLICT], timeout: 5)
if msg == MSG_CONFLICT
puts "integration container already running"
system "docker", "ps", "-f", "name=#{images["integrate"].docker_container_name}"
if !force
raise "cannot run tests; remove container or do so automatically with --force"
end
system "docker", "stop", "-t", "0", images["integrate"].docker_container_name
integration = images["integrate"].command("mount", popen: true)
msg = wait_for("integrate", integration, [MSG_PRESS_ENTER, MSG_CONFLICT], timeout: 5)
if msg == MSG_CONFLICT
raise "still couldn't start container"
end
end
puts "test: checking that touching a file is recognised"
id = 16.times.map { ("a".."z").to_a.sample(1) }.join
images["integrate"].exec("--", "touch", "TestRoot/src/#{id}")
msg = wait_for("integrate", integration, ["OnNewFileCreated (isDirectory: False): #{id}"])
images["integrate"].exec("--", "rm", "TestRoot/src/#{id}")
msg = wait_for("integrate", integration, ["OnPreDelete (isDirectory: False): #{id}"])
puts "test: finished; stopping mount gracefully"
integration.write "\n"
integration.flush
integration.close_write
report("integrate", integration.read)
integration.close
puts "test: done"
end
# vim: set sw=2 et:

18
docker/update-readme Executable file
Просмотреть файл

@ -0,0 +1,18 @@
#!/usr/bin/env ruby
require 'commonmarker'
doc = CommonMarker.render_doc(File.read('README.md'))
did_replace = false
doc.walk do |node|
if !did_replace && node.type == :code_block && node.string_content.start_with?("$ ./projfs")
node.string_content = "$ ./projfs\n" + `./projfs 2>&1`
did_replace = true
end
end
raise "replaced nothing" unless did_replace
File.open("README.md", "w") {|f| f.write(doc.to_commonmark)}

Просмотреть файл

@ -1,8 +0,0 @@
#!/bin/sh
cat <<EOF
noop
(this exists because we need to have a projfs-linux-private project in Janky in
order for projfs-docker to depend on it.)
EOF