Saturday, August 16, 2014

Adding gclient deps

Up until now, we have basically simulated a git clone using gclient. We can now start with an empty directory (and a .gclient file), do a

$ gclient sync

and end up with a clone of whatever solution (project) was specified in the .gclient file.

The next thing I'd like to is to add some dependencies. As you recall, we told gclient config to use git dependencies, which means we need to come up with a .DEPS.git file that specifies the dependencies of the project. I believe that you can use any filename you want, as long as .gclient file reflects that, but don't quote me on that.

So far, here's what I have:

$ ls -1a
.
..
.gclient

$ cat .gclient
solutions = [
  { "name"        : "my_project",
    "url"         : "ssh://example.com/repos/my_project.git",
    "deps_file"   : ".DEPS.git",
    "managed"     : True,
    "custom_deps" : {
    },
    "safesync_url": "",
  },
]
cache_dir = None

$ gclient sync
Syncing projects: 100% (1/1), done.

$ ls -a
.
..
.gclient
.gclient_entries
my_project

There's a new file .gclient_entries, and my project clone (the same thing I would get if I did a git clone ssh://example.com/repos/my_project.git)

For completeness,

$ cat .gclient_entries
entries = {
  'my_project': 'ssh://example.com/repos/my_project.git',
}

That seems to be a simple map of project name (possibly directory) to the git url of the project. I'll just leave that alone.

Now, I'd like to add a dependency to this project. I will probably have a lot of dependencies, but let's just add googletest.

It has its own repository and uses subversion as far as I can tell. Since I prefer to have all of my code and dependencies on one host, I actually went ahead and cloned googletest, then created a git repo on my host (what I refer to as example.com here), and committed it there. So, googletest is available for me form ssh://example.com/repos/external/googletest.git. Note that in the future, it might be worth it to experiment with adding dependency straight from the source, but for now I want to be able to keep track of everything.

Now, to add a .DEPS.git to the main project. Ha! I have no idea what it supposed to look like. Since I know Chromium has a bunch of dependencies, that's probably a good spot to start digging. Conveniently, Chromium has a very useful code search tool that allows us to take a look at the full source code without needing to clone a local copy: cs.chromium.org. Searching for .DEPS.git gets me pretty quickly to this file.

It looks like it has the following structure:

vars = {
 ...
}

deps = {
 ...
}

deps_os = {
 ...
}

include_rules = {
 ...
}

skip_child_includes = {
 ...
}

hooks = {
 ...
}

It also supports comments like most good files. Now, since gclient didn't mind when I didn't have this file, I'm hoping that it doesn't mind if I omit some of the sections. In particular, since I'm only trying to get one dependency to clone, I'm not going to put in anything after deps (ie deps_os, include_rules, etc).

Also, since I want this to be maintainable, I'm going to define convenient vars and use them in deps. Here's my first attempt:

vars = {
  # Common settings.
  "base_url" : "ssh://example.com/repos",

  # Specify dependency package |package| as package_destination,
  # package_url, and package_revision tuples. Then, ensure to
  # add the dependency in deps using the variables.

  # Google test
  "googletest_destination" : "third_party/googletest",
  "googletest_url" : "/external/googletest.git",
  "googletest_revision" : "2a2740e0ce24acaae88fb1c7b1edf5a2289d3b1c",
}

deps = {
  # Google test
  Var("googletest_destination") :
      Var("base_url") + Var("googletest_url") + "@" + Var("googletest_revision")
}

Most of the fields are self explanatory. Googletest_revision refers to the git hash of the latest (and in my case only) git checking. You can get this via git log if you clone the repo separately.

Let's see what sync gets us:

$ gclient sync
Syncing projects: 100% (2/2), done.

That seems to have worked, 2/2 is a good thing, but...

$ ls my_project/
README

my_project only has README that I added to it independently. Let's see what happened. Asking gclient to print more information gets us the following:

$ gclient sync --verbose
solutions = [
  { "name"        : "my_project",
    "url"         : "ssh://example.com/repos/my_project.git",
    "deps_file"   : ".DEPS.git",
    "managed"     : True,
    "custom_deps" : {
    },
    "safesync_url": "",
  },
]
cache_dir = None


my_project (Elapsed: 0:00:01)
----------------------------------------
[0:00:00] Started.
_____ my_project at refs/remotes/origin/master
[0:00:01] Fetching origin
Checked out revision ec01ec9b2387175083549cb155d5aa00a6311ed0
[0:00:01] Finished.
----------------------------------------

third_party/googletest (Elapsed: 0:00:00)
----------------------------------------
[0:00:01] Started.
_____ third_party/googletest at 2a2740e0ce24acaae88fb1c7b1edf5a2289d3b1c
[0:00:01] Up-to-date; skipping checkout.
Checked out revision 2a2740e0ce24acaae88fb1c7b1edf5a2289d3b1c
[0:00:01] Finished.
----------------------------------------

Hmm everything up to date. Ah! The problem is that it seems to have checked out googletest relative to the .gclient file, not relative to the project:

$ ls -1
my_project
third_party

I guess that's useful in some scenarios, but I would really prefer to keep my third_party libs inside my_project directory. That's easy enough to fix. Here's an updated .DEPS.git:

vars = {
  # Common settings.
  "base_url" : "ssh://example.com/repos",
  "project_directory" : "my_project",

  # Specify dependency package |package| as package_destination,
  # package_url, and package_revision tuples. Then, ensure to
  # add the dependency in deps using the variables.

  # Google test
  "googletest_destination" : "third_party/googletest",
  "googletest_url" : "/external/googletest.git",
  "googletest_revision" : "2a2740e0ce24acaae88fb1c7b1edf5a2289d3b1c",
}

deps = {
  # Google test
  Var("project_directory") + "/" + Var("googletest_destination") :
      Var("base_url") + Var("googletest_url") + "@" + Var("googletest_revision")
}

I added a project_directory variable and modified deps to use that as the leading directory before googletest_destination. Let's see if that does it:

$ gclient sync
Syncing projects: 100% (2/2), done.                             

WARNING: 'third_party/googletest' is no longer part of this client.  It is recommended that you manually remove it.

$ ls -1 my_project
README
third_party

That seems to have worked with a useful message reminding me that third_party/googletest (not my_project/third_party/googletest) was removed, so I should clean that up.

Running gclient sync again does not produce the message again. This means that gclient records what dependencies I had on the last run, and warns me if they are no longer in the new .DEPS.git (protip: it's in .gclient_entries). Got it. I'll remove third_party/googletest.

Another cool thing is that I can run gclient sync from any subdirectory and it seems to find the .gclient file. That's kind of useful.

Ok, as a last task let's do a bit of cleanup. From within my_project, I get the following

$ git status
HEAD detached at origin/master
Untracked files:
  (use "git add ..." to include in what will be committed)

 .DEPS.git
 third_party/

nothing added to commit but untracked files present (use "git add" to track)

I'll keep .DEPS.git, so I'll add it to the repo, but I don't want to add third_party or be constantly reminded about it, so I'll put third_party into .gitignore and add that instead.

$ git add .DEPS.git
$ echo third_party > .gitignore
$ git add .gitignore
$ git status
HEAD detached at origin/master
Changes to be committed:
  (use "git reset HEAD ..." to unstage)

 new file:   .DEPS.git
 new file:   .gitignore

Better. "HEAD detached at origin/master" is kind of worrying me though.

$ git commit -a
[detached HEAD 4e6f8b3] Added deps and gitignore
 2 files changed, 22 insertions(+)
 create mode 100644 .DEPS.git
 create mode 100644 .gitignore
$ git push
fatal: You are not currently on a branch.
To push the history leading to the current (detached HEAD)
state now, use

    git push origin HEAD:<name-of-remote-branch>

That sucks, I want to be on master when I sync initially. For now,

$ git push origin HEAD:master
Counting objects: 5, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 661 bytes | 0 bytes/s, done.
Total 4 (delta 0), reused 0 (delta 0)
To ssh://example.com/repos/my_project.git
   ec01ec9..4e6f8b3  HEAD -> master
$ git status
HEAD detached from ec01ec9
nothing to commit, working directory clean
$ gclient sync
Syncing projects: 100% (2/2), done.                             
$ git status
HEAD detached from ec01ec9
nothing to commit, working directory clean
$ git checkout master
Previous HEAD position was 4e6f8b3... Added deps and gitignore
Switched to branch 'master'
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)
$ gclient sync
Syncing projects: 100% (2/2), done.                             
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

nothing to commit, working directory clean

Ok, everything seems to be working. I'm hoping that I can add some sort of a hook to checkout master when syncing, as I don't want to always be remembering to checkout master. But that's for the next post.

- vmpstr

No comments:

Post a Comment