28

Given a file structure like this:

project root
|-- X.sln
|-- src
|    |-- Foo
|    |    |-- Foo.fsproj
|    |    |-- Foo.fs
|    |-- Bar
|         |-- Bar.fsproj
|         |-- Bar.fs
|-- test
     |-- Baz
          |-- Baz.fsproj

I'd like to first add all .fsproj files to my Docker image, then run a command, then add the rest of the files. I tried the following, but of course it didn't work:

COPY X.sln .
COPY **/*.fsproj .
RUN dotnet restore
COPY . .
RUN dotnet build

The idea is that after the first two COPY steps, the file tree on the image is like this:

working dir
|-- X.sln
|-- src
|    |-- Foo
|    |    |-- Foo.fsproj
|    |-- Bar
|         |-- Bar.fsproj
|-- test
     |-- Baz
          |-- Baz.fsproj

and the rest of the tree is only added in after RUN dotnet restore.

Is there a way to emulate this behavior, preferably without resorting to scripts outside of the dockerfile?

Tomas Aschan
  • 58,548
  • 56
  • 243
  • 402
  • Except caching any reason you want to do this? – Tarun Lalwani Aug 20 '17 at 19:56
  • 3
    The main reason is that the `dotnet restore` command usually takes quite a lot of time, so avoiding it when it's not necessary means a considerable reduction of build time. – Tomas Aschan Aug 20 '17 at 20:11
  • I have bash based solution but not a Dockerfile based as currently glob patterns don't work in COPY and neither do multiple .dockerignores – Tarun Lalwani Aug 20 '17 at 20:18
  • @TomasAschan did you find a solution in the meantime? – Kirill Rakhman Aug 20 '19 at 12:09
  • @KirillRakhman No, I'm copying each project file individually still. – Tomas Aschan Aug 20 '19 at 12:44
  • I was also looking for a solution for my F# projects. Then I realized that this approach is much less effective in the F# world, because the project file list every source file. In C# by default all cs files are included in the project, which makes the project file change less likely. Every time you add a source file to your F# project `docker restore` will run anyway. – Philipp Haider Sep 02 '19 at 09:25
  • @PhilippHaider: Yeah, for F# I probably wouldn't bother. – Tomas Aschan Sep 02 '19 at 11:12
  • See also https://andrewlock.net/optimising-asp-net-core-apps-in-docker-avoiding-manually-copying-csproj-files-part-2/ – Michael Freidgeim Dec 11 '20 at 23:41

4 Answers4

7

You can use two RUN commands to solve this problem, using the shell commands (find, sed, and xargs).

Follow the steps:

  1. Find all fsproj files, with regex extract the filename without extension and with xargs use this data to create directories with mkdir;
  2. Based on the previous script change the regex to create from-to syntax and use the mv command to move files to newly created folders.

Example:

    COPY *.sln ./
    COPY */*.fsproj ./
    RUN find *.fsproj | sed -e 's/.fsproj//g' | xargs mkdir
    RUN find *.fsproj | sed -r -e 's/((.+).fsproj)/.\/\1 .\/\2/g' | xargs -I % sh -c 'mv %'

References:

how to use xargs with sed in search pattern

user9785882
  • 71
  • 1
  • 2
  • the sed s command allows you to use a different separator, e.g. '|' instead of '/'. When you do that you don't have to escape the / in the replacement string which makes it more readable. – user829755 Mar 04 '21 at 11:18
7

If you use the dotnet command to manage your solution you can use this piece of code:

  1. Copy the solution and all project files to the WORKDIR
  2. List projects in the solution with dotnet sln list
  3. Iterate the list of projects and move the respective *proj files into newly created directories.
COPY *.sln ./
COPY */*/*.*proj ./
RUN dotnet sln list | \
      tail -n +3 | \
      xargs -I {} sh -c \
        'target="{}"; dir="${target%/*}"; file="${target##*/}"; mkdir -p -- "$dir"; mv -- "$file" "$target"'
Alexander Groß
  • 10,200
  • 1
  • 30
  • 33
0

One pattern that can be used to achieve what you want without resorting to a script outside the Dockerfile is this:

    COPY <project root> .    
    RUN <command to tar/zip the directory to save a copy inside the container> \
         <command the removes all the files you don't want> \
         dotnet restore \
         <command to unpack tar/zip and restore the files> \
         <command to remove the tar/zip> \
         dotnet build

This would keep all of your operations inside the container. I've bundled them all in one RUN command to keep all of that activity into a single layer of the build. You can break them out if you need to.

Here's just one example on linux of how to recursively remove all files except the ones you don't want: https://unix.stackexchange.com/a/15701/327086. My assumption, based on your example, is that this won't be a costly operation for you.

Troy Kelley
  • 114
  • 4
  • 10
    This fails to achieve the _why_ of my original question, though: because you `COPY . .` as the first step, the build context will have changed when I change a source file, forcing me to wait for `dotnet restore` also when no packages have changed. So this doesn't really give any value over naive `COPY . .`, `RUN dotnet restore`, `RUN dotnet build` without caring about copying the project files first. – Tomas Aschan Dec 17 '18 at 08:03
  • You probably need to use a `.dockerignore` file with this to at least filter out ancilliary dirs and files such as `.git`, `Dockerfile` and so forth - any edits will otherwise bust the docker build cache causing extended build times in development. – Ed Randall Oct 15 '21 at 09:33
0

Great question and I believe I have found the solution. Have .dockerignore like this

# Ignore everything except *.fsproj.
**/*.*
!**/*.fsproj

Have your Dockerfile-AA like this (please update the ls)

FROM your-image
USER root
RUN mkdir -p /home/aa
WORKDIR /home/aa
COPY . .
RUN ls
RUN ls src
RUN ls src/p1
RUN ls src/p2
RUN ls src/p3
RUN dotnet restore

Have your docker command like this

sudo docker build --rm -t my-new-img -f Dockerfile-AA .

Run it the first time, it will show only the fsproj file being copied. Run it again, you cannot see the ls result because it is using cache, great.

Obviously, you cannot have dotnet restore in the same Dockerfile, have another docker file like Dockerfile-BB

FROM my-new-img
COPY . .
RUN dotnet build

So, have your script like this

docker build --rm -t my-new-img -f Dockerfile-AA .
rm -f .dockerignore
docker build --rm -t my-new-img2 -f Dockerfile-BB .

This should work. When building my-new-img, it is going to be blazing fast. You need to try this at least 2 times, because the cache was not created right away in my past experience. This is way better than copying the project file line by line.

BoBoDev
  • 847
  • 1
  • 8
  • 17