Random posts about coding

Mostly blogging about dart.

Getting Started With Google Cloud Datastore and Dart

| Comments

NOTE: Experimental and future developments with Dart+GCE+Datastore may change

Before running through the steps below, make sure that:

In order to make API calls to the Datastore, pubspec.yaml file needs the following

1
2
3
dependencies:
  google_oauth2_client: '>=0.3.6 <0.3.7'
  google_datastore_v1beta2_api: ">=0.4.0 <0.5.0"

Then, get the dart_datastore_example sample:

1
2
3
git clone https://github.com/financeCoding/dart_datastore_example.git
cd dart_datastore_example
pub install

If you are not connected to a Compute Engine instance, make sure to run the following commands (in a bash-like shell):

1
2
3
4
5
6
7
8
9
10
# convert the .p12 private key file to a .pem file
# if asked to enter import password, use "notasecret"

openssl pkcs12 -in <privatekey>.p12 -nocerts -passin pass:notasecret -nodes -out <rsa_private_key>.pem

# configure your credentials
export DATASTORE_SERVICE_ACCOUNT=<service-account>
export DATASTORE_PRIVATE_KEY_FILE=<path-to-pem-file>
export CLOUD_PROJECT_ID=<project-id>
export CLOUD_PROJECT_NUMBER=<project-number>

Alternatively the sample allows for passing parameters via commandline:

1
2
3
4
5
cd dart_datastore_example
pub install
cd bin
# dart dart_datastore_example.dart <project-id> <project-number> <path-to-pem-file> <service-account>
dart dart_datastore_example.dart dartcloud 657648630269 privatekey.pem 657648630269-ge2he8e46y4u42bd89nmgtj52j3ilzvv@developer.gserviceaccount.com

Example output on first run:

1
2
3
4
5
6
dartcloud
657648630269
privatekey.pem
657648630269-ge2he8e46y4u42bd89nmgtj52j3ilzvv@developer.gserviceaccount.com
did not found entity
> entity = {question: {"stringValue":"Meaning of life?"}, answer: {"integerValue":42}}

Example output on second run:

1
2
3
4
5
6
dartcloud
657648630269
privatekey.pem
657648630269-ge2he8e46y4u42bd89nmgtj52j3ilzvv@developer.gserviceaccount.com
found entity = {"key":{"partitionId":{"datasetId":"s~dartcloud"},"path":[{"kind":"Trivia","name":"hgtg"}]},"properties":{"question":{"stringValue":"Meaning of life?"},"answer":{"integerValue":42}}}
> entity = {question: {"stringValue":"Meaning of life?"}, answer: {"integerValue":42}}

The comments in the sample’s source explain its behavior in detail:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import "dart:io";

import "package:google_oauth2_client/google_oauth2_console.dart";
import "package:google_datastore_v1beta2_api/datastore_v1beta2_api_client.dart"
    as client;
import "package:google_datastore_v1beta2_api/datastore_v1beta2_api_console.dart"
    as console;

void main(List<String> args) {
  Map<String, String> envVars = Platform.environment;
  String projectId = envVars['CLOUD_PROJECT_ID'] == null ?
      args[0] : envVars['CLOUD_PROJECT_ID'];
  String projectNumber = envVars['CLOUD_PROJECT_NUMBER'] == null ?
      args[1] : envVars['CLOUD_PROJECT_NUMBER'];
  String pemFilename = envVars['DATASTORE_PRIVATE_KEY_FILE'] == null ?
      args[2] : envVars['DATASTORE_PRIVATE_KEY_FILE'];
  String serviceAccountEmail = envVars['DATASTORE_SERVICE_ACCOUNT'] == null ?
      args[3] : envVars['DATASTORE_SERVICE_ACCOUNT'];

  print(projectId);
  print(projectNumber);
  print(pemFilename);
  print(serviceAccountEmail);

  String iss = serviceAccountEmail;
  String scopes = 'https://www.googleapis.com/auth/userinfo.email '
      'https://www.googleapis.com/auth/datastore';
  String rsa_private_key_file = new File(pemFilename).readAsStringSync();

  ComputeOAuth2Console computeEngineClient = new ComputeOAuth2Console(
      projectNumber, privateKey: rsa_private_key_file, iss: iss, scopes: scopes);

  console.Datastore datastore = new console.Datastore(computeEngineClient)
  ..makeAuthRequests = true;

  // Create a RPC request to begin a new transaction
  var beginTransactionRequest = new client.BeginTransactionRequest.fromJson({});
  String transaction;
  client.Key key;
  client.Entity entity;

  // Execute the RPC asynchronously
  datastore.datasets.beginTransaction(beginTransactionRequest, projectId).then(
      (client.BeginTransactionResponse beginTransactionResponse) {
    // Get the transaction handle from the response.
    transaction = beginTransactionResponse.transaction;

    // Create a RPC request to get entities by key.
    var lookupRequest = new client.LookupRequest.fromJson({});

    // Create a new entities by key
    key = new client.Key.fromJson({});

    // Set the entity key with only one `path_element`: no parent.
    var path = new client.KeyPathElement.fromJson({
      'kind': 'Trivia',
      'name': 'hgtg'
    });
    key.path = new List<client.KeyPathElement>();
    key.path.add(path);
    lookupRequest.keys = new List<client.Key>();

    // Add one key to the lookup request.
    lookupRequest.keys.add(key);

    // Set the transaction, so we get a consistent snapshot of the
    // entity at the time the transaction started.
    lookupRequest.readOptions = new client.ReadOptions.fromJson({
      'transaction': transaction
    });

    // Execute the RPC and get the response.
    return datastore.datasets.lookup(lookupRequest, projectId);
  }).then((client.LookupResponse lookupResponse) {
    // Create a RPC request to commit the transaction.
    var req = new client.CommitRequest.fromJson({});

    // Set the transaction to commit.
    req.transaction = transaction;

    if (lookupResponse.found.isNotEmpty) {
      // Get the entity from the response if found
      entity = lookupResponse.found.first.entity;
      print("found entity = ${entity.toString()}");
    } else {
      print("did not found entity");
      // If no entity was found, insert a new one in the commit request mutation.
      entity = new client.Entity.fromJson({});
      req.mutation = new client.Mutation.fromJson({});
      req.mutation.insert = new List<client.Entity>();
      req.mutation.insert.add(entity);

      // Copy the entity key.
      entity.key = new client.Key.fromJson(key.toJson());

      // Add two entity properties:

      // - a utf-8 string: `question`
      client.Property property = new client.Property.fromJson({});
      property.stringValue = "Meaning of life?";
      entity.properties = new Map<String, client.Property>();
      entity.properties['question'] = property;

      // - a 64bit integer: `answer`
      property = new client.Property.fromJson({});
      property.integerValue = 42;
      entity.properties['answer'] = property;

      // Execute the Commit RPC synchronously and ignore the response:
      // Apply the insert mutation if the entity was not found and close
      // the transaction.
      return datastore.datasets.commit(req, projectId);
    }
  }).then((client.CommitResponse commitResponse) =>
      print("> entity = ${entity.properties}"));
}

With this example, you learned how to use the:

Now, you are ready to learn more about the Key Datastore Concepts and look at the JSON API reference.


Example deployment and startup scripts for GCE with Dart

setup-instance.sh script creates the node with the right correct scopes and image.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env bash
set +o xtrace

USER=$USER
PROJECT=dart-compute-project
INSTANCE_NAME=dart-compute
TAGS=dart
MACHINE_TYPE=f1-micro
NETWORK=default
IP=ephemeral
IMAGE=https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/backports-debian-7-wheezy-v20140318
SCOPES=https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/compute,https://www.googleapis.com/auth/devstorage.full_control,https://www.googleapis.com/auth/datastore
PERSISTENT_BOOT_DISK=true
AUTO_DELETE_BOOT_DISK=true
ZONE=us-central1-b
STARTUP_SCRIPT=startup-script.sh
GCUTIL="gcutil --service_version=v1 --project=$PROJECT"

$GCUTIL addinstance $INSTANCE_NAME --tags=$TAGS --zone=$ZONE --machine_type=$MACHINE_TYPE --network=$NETWORK --external_ip_address=$IP --service_account_scopes=$SCOPES --image=$IMAGE --persistent_boot_disk=$PERSISTENT_BOOT_DISK --auto_delete_boot_disk=$AUTO_DELETE_BOOT_DISK --metadata_from_file=startup-script:$STARTUP_SCRIPT

rc=$?
if [[ $rc != 0 ]] ; then
  echo "Not able to add instance"
    exit $rc
fi

startup-script.sh script that provisions the node with dart.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env bash

# Add an addtional source for the latest glibc
sudo sed -i '1i deb http://ftp.us.debian.org/debian/ jessie main' /etc/apt/sources.list

# Update sources
sudo apt-get update

# Download latest glibc
sudo DEBIAN_FRONTEND=noninteractive apt-get -t jessie install -y libc6 libc6-dev libc6-dbg git screen unzip vim

# Download the latest dart sdk
wget http://storage.googleapis.com/dart-archive/channels/dev/release/latest/sdk/dartsdk-linux-x64-release.zip -O dartsdk-linux-x64-release.zip 

# Unpack the dart sdk
unzip -d / dartsdk-linux-x64-release.zip

# Make the sdk readable
chmod -R go+rx /dart-sdk

# Add dart bin to global path
echo "export PATH=\$PATH:/dart-sdk/bin" >> /etc/profile

chrome.dart 0.4.0 Release

| Comments

The dart-gde has updated the chrome.dart to ‘0.4.0’. This release contains a whole rewrite of the project. All APIs are now generated from chrome apps/ext IDLs. Most unit tests from previous chrome.dart have been ported.

About chrome.dart

chrome.dart package provides the generated APIs to access chrome apps & extensions through dart interfaces. The APIs depend on the js-interop to communicate with the javascript vm for accessing the APIs. Currently no native interfaces exist between dart vm and chrome browser. In a later point in time those interfaces will exist, for now this provides a way to access those interfaces.

chrome_gen.dart -> chrome.dart

At first chrome_gen.dart was the project that kicked off moving to generated APIs. It was getting frustrating having to hand write, maintain and test chrome.dart. We did avoid looking into generation from IDL, because the IDL in the chromium is in two different formats json & chrome specific WebIDL. The nice part about the original chrome.dart package is we could introduce more structure for the APIs then what the generated javascript APIs looked like. So after initial development by Devon Carew, we decided it was a better direction to use parsers and generators to create and maintain the APIs. Four months later we had a 90% complete package that was already being used by a few projects, including spark. We are able to generate enough structure to keep nice APIs and generate form the IDL, this was a win for the project.

Documentation

The API documentation is automatically generated for Apps and Extensions on each check in. A few wiki pages exist and we are in the process of migrating them from chrome_gen.dart wiki -> chrome.dart wiki. We do need help and welcome pull requests and wiki edits.

Samples

Currently the best source of project setup exists with app in the project. The app folder in a dart project will not have the automatic symlinks generated by pub. That helps out greatly since it would pollute our folder structure and cause bad things to happen. Instead we have a simple setup_app.dart script that helps build and copy packages over to the app folder. Right now its specific for the chrome.dart project but should be easy to replicate for your own chrome apps or extensions. Another neat script which is mac specific but could be generalized is load_app.sh. load_app.sh shows a simple way to load up an application in app folder from command line.

Helping out

We still need more unit tests, documentation, and additional development. If your interested checkout the github issues or send pull requests.

Thanks!

To all the contributors on this project Devon Carew, Kevin Moore, Ross Smith, Marc Fisher

Build and Deploy Dart to Beaglebone Black

| Comments

I was looking into using dart on Beaglebone Black and decided it would be useful to share with others what I found.

After a some build hacks and patches I found a minimal working solution for Beaglebone Black with Debian ARMhf. A few important notes before going down the road of building dart for ARM. The dart-sdk is not fully supported and pub currently might not work. The dartanalyzer might not work. The only supported ARM architectures are the ones that have ARMv7 with VFP. Don’t spin your wheels trying to target any architecutre that is not ARMv7 with VFP (minimum at the moment ARMv7-A) unless you plan on implementing the routines needed in the runtime arm assembler. If you do plan on implementing them, well thats just pure awesome!

assembler_arm.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CPUFeatures::InitOnce() {
#if defined(USING_SIMULATOR)
  integer_division_supported_ = true;
  neon_supported_ = true;
#else
  ASSERT(CPUInfoContainsString("ARMv7"));  // Implements ARMv7.
  ASSERT(CPUInfoContainsString("vfp"));  // Has floating point unit.
  // Has integer division.
  if (CPUInfoContainsString("QCT APQ8064")) {
    // Special case for Qualcomm Krait CPUs in Nexus 4 and 7.
    integer_division_supported_ = true;
  } else {
    integer_division_supported_ = CPUInfoContainsString("idiva");
  }
  neon_supported_ = CPUInfoContainsString("neon");
#endif  // defined(USING_SIMULATOR)
#if defined(DEBUG)
  initialized_ = true;
#endif
}

Download Ubuntu 12.04.3 LTS

Download the desktop iso to install on VirtualBox.

Install on VirtualBox

I work mostly on mac so Ubuntu installed on VirtualBox was needed to help with cross compiling and flashing of uSD cards.

Update the packages

Just to be safe update any Ubuntu packages before installing the development software.

Install basic packages

I typically use the git overlay on subversion when working with the dart repo. The java jre/jdk is required for building the dart_analyzer which does not work in the sdk.

shell
1
2
# Install subversion 
sudo apt-get install subversion git git-svn openssh-server vim default-jre default-jdk

Install the chrome build and arm dependencies

Checkout the latest build tools scripts. The following scripts prep your system with any packages needed for building dart.

shell
1
2
3
4
5
6
7
# checkout build scripts
svn co http://src.chromium.org/chrome/trunk/src/build; cd build

# install dependencies
chmod u+x install-build-deps.sh
./install-build-deps.sh --no-chromeos-fonts
./install-build-deps.sh --no-chromeos-fonts --arm

Install addtional libraries

The following libraries are needed for building dart but might not be included from the chrome build tool scripts.

shell
1
2
# Install addtional libs
sudo apt-get install libc6-dev-i386 g++-multilib

Install depot-tools

depot-tools is required for hacking out the dart source code.

shell
1
2
3
# depot tools
svn co http://src.chromium.org/svn/trunk/tools/depot_tools
export PATH=$PATH:`pwd`//depot_tools

Checkout the dart code base

You dont need to include --username <YOUR USERNAME> unless you plan on creating a CL for review.

shell
1
2
3
4
5
6
7
mkdir dart_bleeding
cd dart_bleeding
svn ls https://dart.googlecode.com/svn/branches/bleeding_edge/ --username <YOUR USERNAME>
gclient config https://dart.googlecode.com/svn/branches/bleeding_edge/deps/all.deps
git svn clone -rHEAD https://dart.googlecode.com/svn/branches/bleeding_edge/dart dart
gclient sync
gclient runhooks

Patch the gyp and version files

A git patch can be found here 7725354, that patches the build scripts to support building the dart-sdk for ARM. Patching the VERSION file was done in an attempt to get pub working. At the moment its not required. If not done then an old version number is baked into the dartvm. This patch also modifies which dartvm creates the snapshots for pub, dart2js and a wrapper util. Patch creates the requirement of having to build the dartvm for x64 before building the dart-sdk for ARM. The dart build scripts have a funky dependency of wanting to use the dartvm target to create the snapshot files. Which in this case wont work since our dartvm is an ARM target being built on x64.

arm.build.patch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
diff --git a/tools/VERSION b/tools/VERSION
index d1ab212..0d6101d 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -1,5 +1,5 @@
 CHANNEL be
-MAJOR 0
-MINOR 1
-BUILD 2
-PATCH 0
+MAJOR 1
+MINOR 0
+BUILD 0
+PATCH 7
diff --git a/utils/compiler/compiler.gyp b/utils/compiler/compiler.gyp
index 294c7e9..5f3754a 100644
--- a/utils/compiler/compiler.gyp
+++ b/utils/compiler/compiler.gyp
@@ -18,7 +18,7 @@
         {
           'action_name': 'generate_snapshots',
           'inputs': [
-            '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)dart<(EXECUTABLE_SUFFIX)',
+            '<(PRODUCT_DIR)/../DebugX64/dart',
             '../../sdk/lib/_internal/libraries.dart',
             '<!@(["python", "../../tools/list_files.py", "\\.dart$", "../../sdk/lib/_internal/compiler", "../../runtime/lib", "../../sdk/lib/_internal/dartdoc"])',
             'create_snapshot.dart',
@@ -30,7 +30,7 @@
             '<(SHARED_INTERMEDIATE_DIR)/dart2js.dart.snapshot',
           ],
           'action': [
-            '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)dart<(EXECUTABLE_SUFFIX)',
+            '<(PRODUCT_DIR)/../DebugX64/dart',
             'create_snapshot.dart',
             '--output_dir=<(SHARED_INTERMEDIATE_DIR)',
             '--dart2js_main=sdk/lib/_internal/compiler/implementation/dart2js.dart',
diff --git a/utils/pub/pub.gyp b/utils/pub/pub.gyp
index fd5e147..ab2e243 100644
--- a/utils/pub/pub.gyp
+++ b/utils/pub/pub.gyp
@@ -25,7 +25,7 @@
             '<(SHARED_INTERMEDIATE_DIR)/pub.dart.snapshot',
           ],
           'action': [
-            '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)dart<(EXECUTABLE_SUFFIX)',
+            '<(PRODUCT_DIR)/../DebugX64/dart',
             '--package-root=<(PRODUCT_DIR)/packages/',
             '--snapshot=<(SHARED_INTERMEDIATE_DIR)/pub.dart.snapshot',
             '../../sdk/lib/_internal/pub/bin/pub.dart',

Build the dart-sdk

Building of the dart-sdk for ARM target is a two stop process. First build x64 so we can use that dartvm to generate the snapshot files. Then the second step is running the create_sdk build for ARM. When the build is finished the out/ReleaseARM/dart-sdk should contain a full dart-sdk build. Keep in mind this does build the dartanalyzer but it may not work on ARM.

shell
1
2
3
4
5
# build a target for your native system to create the snapshot files. 
./tools/build.py -m debug -v -a x64 -j 8

# build the arm target
./tools/build.py -m release -v -a arm -j 8 create_sdk

Tarball the sdk

Package up the dart-sdk as a tarball to distribute.

shell
1
2
cd ./out/ReleaseARM/
tar -czvf dart-sdk.tar.gz dart-sdk

Install Debian Wheezy 7.2 Hard Float Minimal Image on Beaglebone Black

In virtualbox with a uSD card at /dev/sdX the following will download an image and write to the uSD card. Updated images can be found at armhf

shell
1
2
wget http://s3.armhf.com/debian/wheezy/bone/debian-wheezy-7.2-armhf-3.8.13-bone30.img.xz
xz -cd debian-wheezy-7.2-armhf-3.8.13-bone30.img.xz > /dev/sdX

Then insert the uSD card into the Beaglebone Black and boot the image by holding down the boot switch and powering on.

Write the booted image to the eMMC.

shell
1
xz -cd debian-wheezy-7.2-armhf-3.8.13-bone30.img.xz > /dev/mmcblk1

Power down and remove the uSD card.

Update glibc on the BeagleBone Black

Updating glibc is required cause the version of glibc installed from the chromium build scripts is greater then the one shipped with Wheezy 7.2. The following commands update glibc.

shell
1
2
3
4
5
6
7
8
# Add an addtional source for the latest glibc
sudo sed -i '1i deb http://ftp.us.debian.org/debian/ jessie main' /etc/apt/sources.list

# Update sources 
sudo apt-get update

# Download latest glibc
sudo DEBIAN_FRONTEND=noninteractive apt-get -t jessie install -y libc6 libc6-dev libc6-dbg git screen

Copy over dart-sdk

From virtual box copy over the tarball to Beaglebone Black running debian.

shell
1
scp dart-sdk.tar.gz debian@192.168.2.2:~/

After the tarball is copied, uncompress and add to your PATH.

shell
1
2
3
4
tar -zxvf dart-sdk.tar.gz
export PATH=~/dart-sdk:$PATH
dart --version
Dart VM version: 1.0.0.7_r30634_adam (Fri Nov 29 01:14:42 2013) on "linux_arm"

Known issues at the moment

Pub does not work, issue could be followed at 15383. I was testing this out while staying at a hotel so some proxy settings might of been blocking or tripping up pub.

Feedback

If you have a better way of running dart on Beagleblone Black I would love to hear it! Please contact me on g+ and lets discuss.

Update on dartanalyzer

dartanalyzer will work after installing the default-jre on Beaglebone Black.

shell
1
sudo apt-get install default-jre

Addtional resources

Google Client Apis New Release 0.4.x

| Comments

The dart-gde team has updated the google client apis to '>=0.4.0'. Along with this change was an update for Google OAuth2 Client to '>=0.3.0'.

The breaking changes for Google OAuth2 Client

  • SystemCache has been removed.
  • GoogleOAuth2.ensureAuthenticated() A much cleaner impl that eliminates the need to pass in a HttpRequest object to authenticate.
  • All dependencies bumped to latest versions.
  • Code refactored.
  • Dead code eliminated.
  • Remove deprecated libraries.
  • Heavy logging removed.

The breaking changes for generated google client apis include

  • Renamed lib/src/{cloud_api.dart -> client_base.dart} lib/src/{cloud_api_console.dart -> console_client.dart} lib/src/{cloud_api_browser.dart -> browser_client.dart}.
  • ClientBase.responseParse(int statusCode, String responseBody) introduced and handles parsing responseBody. responseParse will throw DetailedApiRequestError if the body has an error.
  • Renamed APIRequestException -> APIRequestError.
  • Remove deprecated libraries.

Updating requires a small change in pubspec.yaml

pubspec.yaml
1
2
dependencies:
  google_plus_v1_api: '>=0.4.0'

A small collection of demo examples could be found at dart_api_client_examples.

Full list of available Google client apis on pub.dartlang.org

Getting Started With Dart on Compute Engine

| Comments

Quick how-to on using dart with Compute Engine. If not aware, right now is a great time to dive into Compute Engine cause google is giving away $2,000 worth of credits to individuals interested in trying it out. I’ve been using it for about 2-3 months now and totally love it.

What peeked my interest is it was flexable enough to run the dartvm as a server with minimal configuration. The one configuration hurdle was dependency of GLIBC >= 2.15 in the dartvm binaries. The good news is with a simple startup script the compute engine instance can be provisioned to support the latest linux dart-sdk.

The main tool we will use to provision a compute engine instance is gcutil. We could of used dartvm and google_compute_v1beta15_api but will save that for a later post.

After signing up for Compute Engine the next step should be to download and configure gcutil.

1
2
3
4
5
$ wget https://google-compute-engine-tools.googlecode.com/files/gcutil-1.8.4.tar.gz
$ tar xzvpf gcutil-1.8.4.tar.gz -C $HOME
$ export PATH=./gcutil-1.8.4:$PATH
$ gcutil version
1.8.4

Next we want to create a startup.sh script that will be deployed to the compute engine instance. The script is a simple way to run additional commands to provision the instance. For dart we need to add a new deb source, update sources, install dependencies, fetch & unpack dart-sdk, and then execute our dart server. In the final line of the startup.sh script the command will create a dart server from the user account tied to this compute instance. Simply we clone a public git repo, install pub dependencies and screen a detached session that runs the dart server. This is not a very fancy way to deploy dart but a simple and quick way to get something running with no troubles. A real life deployment might include some trendy fab/chef/puppet combo.

startup.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env bash

# Add an addtional source for the latest glibc
sudo sed -i '1i deb http://ftp.us.debian.org/debian/ jessie main' /etc/apt/sources.list

# Update sources 
sudo apt-get update

# Download latest glibc
sudo DEBIAN_FRONTEND=noninteractive apt-get -t jessie install -y libc6 libc6-dev libc6-dbg git screen

# Download the latest dart sdk
wget https://storage.googleapis.com/dart-editor-archive-integration/latest/dartsdk-linux-64.tar.gz -O /dartsdk-linux-64.tar.gz

# Unpack the dart sdk
tar -zxvf /dartsdk-linux-64.tar.gz -C /

su - financeCoding -c 'ls -al && cd ~ && pwd && git clone https://github.com/rikulo/stream.git && /dart-sdk/bin/dart --version && cd stream && /dart-sdk/bin/dart --version && /dart-sdk/bin/pub install && cd example/hello-static && screen -d -m /dart-sdk/bin/dart webapp/main.dart'

After we have the startup.sh script we then create another deployment script. The following script will be the gcutil commands needed to actually create and provision the compute instance. The last part of our script includes a firewall rule for the port that the stream sample is running on. Without proper firewall rules no access from the outside is possible.

deploy-dart-compute.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env bash
set +o xtrace

USER=financeCoding
PROJECT=dart-compute-project
INSTANCE_NAME=dart-compute
TAGS=dart
MACHINE_TYPE=g1-small
NETWORK=default
IP=ephemeral
IMAGE=https://www.googleapis.com/compute/v1beta15/projects/debian-cloud/global/images/debian-7-wheezy-v20130816
SCOPES=https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/compute,https://www.googleapis.com/auth/devstorage.full_control
PERSISTENT_BOOT_DISK=true
ZONE=us-central1-b
STARTUP_SCRIPT=startup.sh
GCUTIL="gcutil --service_version=v1beta15 --project=$PROJECT"

$GCUTIL addinstance $INSTANCE_NAME --tags=$TAGS --zone=$ZONE --machine_type=$MACHINE_TYPE --network=$NETWORK --external_ip_address=$IP --service_account_scopes=$SCOPES --image=$IMAGE --persistent_boot_disk=$PERSISTENT_BOOT_DISK --metadata_from_file=startup-script:$STARTUP_SCRIPT

rc=$?
if [[ $rc != 0 ]] ; then
  echo "Not able to add instance"
    exit $rc
fi

$GCUTIL addfirewall $INSTANCE_NAME --allowed "tcp:8080"

rc=$?
if [[ $rc != 0 ]] ; then
  echo "Not able to provision firewall or has already been provisioned"
    exit $rc
fi

exit $rc

compute-engine-console

stream-client

And thats all that is needed to get dart on compute engine in two easy steps. The code can be found here gist.

Gplus Quickstart With Dart

| Comments

Tonights mash-up was taking the gplus-quickstart-dart and wiring it up for server side support. Similar to the gplus-quickstart-java, the client will use the gplus login button to do the OAuth2WebServer flow and send the code over to the server. The server can then verify and make calls on behalf of the client since an ‘offline’ token was requested. This demo just features the server side and what was used to put it together. Yulian Kuncheff has been the primary developer behind fukiya which is an express like framework for dart. The thing I liked most about fukiya was how simple and easy it was to setup URL handlers.

First off, setting up some dependencies.

1
2
3
4
5
6
dependencies:
  google_plus_v1_api: any
  browser: any
  fukiya: '>=0.0.11'
  html5lib: ">=0.4.1 <0.4.2"
  logging: ">=0.4.3+5"

A quick outline of what URLs fukiya handles. Dead simple to setup!

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
  new Fukiya()
  ..get('/', getIndexHandler)
  ..get('/index.html', getIndexHandler)
  ..get('/index', getIndexHandler)
  ..post('/connect', postConnectDataHandler)
  ..get('/people', getPeopleHandler)
  ..post('/disconnect', postDisconnectHandler)
  ..staticFiles('./web')
  ..use(new FukiyaJsonParser())
  ..listen('127.0.0.1', 3333);
}

The index handler is special cause we needed to inject a state token into the page and HTTP session. The state token is then verified on the /connect post. The one-time token helps avoid any Confused_deputy_problems.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void getIndexHandler(FukiyaContext context) {
  // Create a state token. 
  context.request.session["state_token"] = _createStateToken();

  // Readin the index file and add state token into the meta element. 
  var file = new File(INDEX_HTML);
  file.exists().then((bool exists) {
    if (exists) {
      file.readAsString().then((String indexDocument) {
        Document doc = new Document.html(indexDocument);
        Element metaState = new Element.html('<meta name="state_token" content="${context.request.session["state_token"]}">');
        doc.head.children.add(metaState);
        context.response.writeBytes(doc.outerHtml.codeUnits);
        context.response.done.catchError((e) => serverLogger.fine("File Response error: ${e}"));
        context.response.close();
      }, onError: (error) => serverLogger.fine("error = $error"));
    } else {
      context.response.statusCode = 404;
      context.response.close();
    }
  });
}

On the /connect post we will expect a gplus id to be passed to the query parameters and some token data posted. We can then verify the state token and use the token data for accessing the Google APIs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void postConnectDataHandler(FukiyaContext context) {
  serverLogger.fine("postConnectDataHandler");
  String tokenData = context.request.session.containsKey("access_token") ? context.request.session["access_token"] : null; // TODO: handle missing token
  String stateToken = context.request.session.containsKey("state_token") ? context.request.session["state_token"] : null;
  String queryStateToken = context.request.queryParameters.containsKey("state_token") ? context.request.queryParameters["state_token"] : null;

  // Check if the token already exists for this session. 
  if (tokenData != null) {
    context.send("Current user is already connected.");
    return;
  }

  // Check if any of the needed token values are null or mismatched.
  if (stateToken == null || queryStateToken == null || stateToken != queryStateToken) {
    context.response.statusCode = 401;
    context.send("Invalid state parameter.");
    return;
  }

  // Normally the state would be a one-time use token, however in our
  // simple case, we want a user to be able to connect and disconnect
  // without reloading the page.  Thus, for demonstration, we don't
  // implement this best practice.
  context.request.session.remove("state_token");

  String gPlusId = context.request.queryParameters["gplus_id"];
  StringBuffer sb = new StringBuffer();
  // Read data from request.
  context.request
  .transform(new StringDecoder())
  .listen((data) => sb.write(data), onDone: () {
    serverLogger.fine("context.request.listen.onDone = ${sb.toString()}");
    Map requestData = JSON.parse(sb.toString());

    Map fields = {
              "grant_type": "authorization_code",
              "code": requestData["code"],
              // http://www.riskcompletefailure.com/2013/03/postmessage-oauth-20.html
              "redirect_uri": "postmessage",
              "client_id": CLIENT_ID,
              "client_secret": CLIENT_SECRET
    };

    http.Client _httpClient = new http.Client();
    _httpClient.post(TOKEN_ENDPOINT, fields: fields).then((http.Response response) {
      // At this point we have the token and refresh token.
      var credentials = JSON.parse(response.body);
      _httpClient.close();

      var verifyTokenUrl = '${TOKENINFO_URL}?access_token=${credentials["access_token"]}';
      new http.Client()
      ..get(verifyTokenUrl).then((http.Response response)  {
        serverLogger.fine("response = ${response.body}");

        var verifyResponse = JSON.parse(response.body);
        String userId = verifyResponse.containsKey("user_id") ? verifyResponse["user_id"] : null;
        String accessToken = credentials.containsKey("access_token") ? credentials["access_token"] : null;
        if (userId != null && userId == gPlusId && accessToken != null) {
          context.request.session["access_token"] = accessToken;
          context.send("POST OK");
        } else {
          context.response.statusCode = 401;
          context.send("POST FAILED ${userId} != ${gPlusId}");
        }
      });
    });
  });
}

Now the HTTP session has the full ability to make calls on behalf of the user. The /people method will be called from the client to retrieve the list of visible friends of that user.

1
2
3
4
5
6
7
8
9
10
void getPeopleHandler(FukiyaContext context) {
  String accessToken = context.request.session.containsKey("access_token") ? context.request.session["access_token"] : null;
  SimpleOAuth2 simpleOAuth2 = new SimpleOAuth2()..credentials = new console_auth.Credentials(accessToken);
  plus.Plus plusclient = new plus.Plus(simpleOAuth2);
  plusclient.makeAuthRequests = true;
  plusclient.people.list("me", "visible").then((plus.PeopleFeed people) {
    serverLogger.fine("/people = $people");
    context.send(people.toString());
  });
}

The final responsibility we can bestow upon the server is allowing the client to disconnect by revoking OAuth access.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void postDisconnectHandler(FukiyaContext context) {
  String tokenData = context.request.session.containsKey("access_token") ? context.request.session["access_token"] : null;
  if (tokenData == null) {
    context.response.statusCode = 401;
    context.send("Current user not connected.");
    return;
  }

  final String revokeTokenUrl = "${TOKEN_REVOKE_ENDPOINT}?token=${tokenData}";
  context.request.session.remove("access_token");

  new http.Client()..get(revokeTokenUrl).then((http.Response response) {
    context.request.session["state_token"] = _createStateToken();
    Map data = {
                "state_token": context.request.session["state_token"],
                "message" : "Successfully disconnected."
                };
    context.send(JSON.stringify(data));
  });
}

Thats about it, Happy Dart Hacking! Special thanks to Gerwin Sturm for putting together the original example for client side. Full source code can be found at gplus-quickstart-dart in the server folder. Please replace your own keys cause mine will be removed at some point.

Dart Multi Touch Canvas With Realtime APIs

| Comments

Google has made the realtime api available for developers. Realtime api provides operational transformation on strings, lists, maps and custom objects. The application data gets stored on Google Drive and is available from any supported browser. This is going to be the tooling of the future for collaborative applications.

I took some time to see what it would take for implementing a sample realtime application in dart. Also wanted to make sure my sample could run on mobile chrome.

Since realtime api is new, dart bindings don’t really exist. Lucky for us we have js-interop library. The js-interop library provides communications to existing javascript code from dart. I consider this mostly a quick hack to get started with the realtime api until a more native interface exists.

The sample realtime_touch_canvas demonstrates a multi touch canvas surface that updates in realtime with all clients that have the application open.

Most of the heavy lifting is done by rtclient.dart. I ported the code from the javascript version. Its enough code to get started right away but a more structured solution should be done. The main class is RealTimeLoader used for realtime loading.

1
2
3
4
5
  rtl = new RealTimeLoader(clientId: 'CLIENTID.apps.googleusercontent.com', apiKey: 'KEY');
  rtl.start().then((bool isComplete) {
    /* RealTimeLoader has authenticated the application and is ready to load a file */
    loadRealTimeFile(fileId, model.onFileLoaded, model.initializeModel);
  });

model.onFileLoaded and model.initializeModel handle the creating of model data and loading of model data.

In the realtime_touch_canvas, model data was a simple list of json strings. The ticky part here is you need to remember that your working with the realtime api within the javascript vm. So an array needs to be allocated from js-interop.

1
2
3
4
  void _createNewModel(js.Proxy model) {
    var list = model.createList(js.array(_defaultLines));
    model.getRoot().set(_linesName, list);
  }

After the model is created we then get called to load the file. Loading the file for our purposes is binding the collaborative objects. Some tricky things to note here is we are retaining the javascript objects so we can access them after exit of the callback. Also the callbacks have to be wrapped within js-interop js.Callback.many proxy object. The callbacks _linesOnAddValuesChangedEvent and _linesOnRemovedValuesChangedEvent are fired off when the collaborative list object has items added or removed.

1
2
3
4
5
6
7
8
9
10
11
12
  js.Proxy _doc;
  String _linesName = "lines";
  js.Proxy _lines;

  void _bindModel(js.Proxy doc) {
    _doc = doc;
    js.retain(_doc);
    _lines = doc.getModel().getRoot().get(_linesName);
    _lines.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, new js.Callback.many(_linesOnAddValuesChangedEvent));
    _lines.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, new js.Callback.many(_linesOnRemovedValuesChangedEvent));
    js.retain(_lines);
  }

When the callback is called the data would be in the javascript virtual machine so we can parse it and store in our native dart code. This is more of a convenience then a must do, that way we can expose plan old dart objects to our other parts of the dart application.

1
2
3
4
5
  void _linesOnAddValuesChangedEvent(addedValue) {
    var insertedLine = _lines.get(addedValue.index);
    var line = new Line.fromJson(insertedLine);
    realtimeTouchCanvas.move(line, line.moveX, line.moveY);
  }

Now when we want to store a line in the application we simply convert it to json and push it into the collaborative list. The little tick here is to make sure we are scoped when accessing the _lines object since it lives in the javascript virtual machine.

1
2
3
4
5
  void addLine(Line line) {
    js.scoped(() {
      _lines.push(line.toJson());
    });
  }

The realtime_touch_canvas is live on github gh-pages and realtime_touch_canvas source is available.

Rikulo Stream on Heroku

| Comments

Tonights hacking was with stream and heroku. Stream is a Dart web server supporting request routing, filtering, template technology, file-based static resources and MVC design pattern. I just planned on serving static content from heroku using full dart based web server.

First setup the dart build pack

shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
adam@Adams-MacBook-Air:~/dart
$ mkdir stream_todomvc

adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ cd stream_todomvc

adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ heroku create stream-todomvc

adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ heroku config:add BUILDPACK_URL=https://github.com/igrigorik/heroku-buildpack-dart.git

adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ git init

adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ git remote add heroku git@heroku.com:stream-todomvc.git

Creating a new project called stream-todomvc. Going to use the todomvc from the web-ui project as our content for the stream server. First thing that should be done is adding the dependencies to the pubspec.yaml file.

pubspec.yaml
1
2
3
4
5
6
7
name: stream_todomvc
description: A sample WebUI application
dependencies:
  browser: any
  js: any
  web_ui: 0.4.1+7
  stream: 0.5.5+1

Next I simply compied the existing todomvc project out into my stream-todomvc project.

shell
1
2
adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ cp ~/dart/web-ui/example/todomvc/* ./web/

stream intro documentation goes over some basic configurations and settings. I’m just going to use them for now to get something running right away. The key to note when serving code from the web/ folder in dart projects is having the stream server code in web/webapp/. That way stream can find all your resources with little configuration. With very little dart code we can have static web server going.

web/webapp/server.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
library server;

import 'dart:io';
import "package:stream/stream.dart";

void main() {
  var port = Platform.environment.containsKey('PORT') ? int.parse(Platform.environment['PORT']) : 8080;
  var host = '0.0.0.0';
  var streamServer = new StreamServer();
  streamServer
  ..port = port
  ..host = host
  ..start();
}

Since this was a web-ui project we need to have a build.dart file help us with transforming the polyfill web components.

build.dart
1
2
3
4
import 'dart:io';
import 'package:web_ui/component_build.dart';

main() => build(new Options().arguments, ['web/index.html']);

The heroku environment requires a procfile configuration to let the service know the type of commands to run.

Procfile
1
web: ./dart-sdk/bin/dart --package-root=./packages/ web/webapp/server.dart

Next we build all the static data for our webapp to function. This will include calling build.dart and dart2js. The second step of calling dart2js helps with clients that do not have the dartvm built in.

shell
1
2
3
4
5
6
7
8
9
10
11
12
13
adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ pub install
Resolving dependencies...
Dependencies installed!

adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ dart build.dart
Total time spent on web/index.html                           -- 839 ms
Total time                                                   -- 863 ms

adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ dart2js -oweb/out/index.html_bootstrap.dart.js web/out/index.html_bootstrap.dart
Using snapshot /Users/adam/Documents/DartEditor/dart/dart-sdk/lib/_internal/compiler/implementation/dart2js.dart.snapshot

Now everything should be ready for deployment.

shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ git add -a -m "ready for deploy"

adam@Adams-MacBook-Air:~/dart/stream_todomvc
$ git push -v --set-upstream heroku master:master
Pushing to git@heroku.com:stream-todomvc.git
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 283 bytes, done.
Total 3 (delta 2), reused 0 (delta 0)

-----> Fetching custom git buildpack... done
-----> Dart app detected
-----> Installing Dart VM, build: latest
-----> Copy Dart binaries to app root
-----> Install packages
*** Found pubspec.yaml in .
Resolving dependencies...
Dependencies installed!
Fixed web symlink
-----> Discovering process types
       Procfile declares types -> web

-----> Compiled slug size: 8.9MB
-----> Launching... done, v7
       http://stream-todomvc.herokuapp.com deployed to Heroku

To git@heroku.com:stream-todomvc.git
   042f1f4..b35984b  master -> master
updating local tracking ref 'refs/remotes/heroku/master'

Deploying to heroku in this style is just a good starting point. web-ui and dart in general is still working on a deployment story. The URL for the stream-todomvc will contain out in its location, not very desirable. In the future a buildtool will aid the deployment story for dart.

Check out the live version of stream-todomvc with full source code available at the stream-todomvc github project.

Dart Google Client Apis Now Available on Pub

| Comments

The dart-gde team now brings you not only a generator to create google client apis but also pub.dartlang.org hosted packages. Lot of thanks goes to Gerwin Sturm for all of his hard work over the last few weeks developing discovery_api_dart_client_generator and dart-google-oauth2-library.

We plan to keep the client libraries up to date with uploader.dart script. Still somewhat a manual process, future automation could happen when/if we have the ability to get notified about google api changes. For now we will push updates when appropriate. This will ensure that we can push the latest versions of the apis to pub and still have previous revisions available. Some of the more intricate parts of this script include auto version incrementing pubspec files and syncing to github, then pushing to pub.

Would you want to contribute to this project? Please feel free to ping us Adam Singer/Gerwin Sturm on g+, we’re definitely looking to refactor some parts and test others. Our main focus for this release was to get something out the door that is pleasantly usable.

Many hours of testing and development was done to have a simple and easy way to use the google client apis in dart! We hope you enjoy and look forward to seeing what you build. To get started with client api examples check out dart_api_client_examples. The github hosted source code can be found at dart-google-apis

Full list of available Google client apis on pub.dartlang.org

Using createDartAnalyzerTask in bot.dart to Ensure Sanity

| Comments

drone.io has really proven to be useful for dart projects. One common pattern I find my self doing with projects these days is running the dart_analyzer before running unit tests. Had a discussion with Kevin Moore about having a dart_analyzer task included in hop, part of the bot.dart framework. We both agreed it would be nice to automate that task and not have shell scripts running the show. Thus createDartAnalyzerTask was born. createDartAnalyzerTask allows you to add dart scripts that are libraries or main entry points to be analyzed by dart_analyzer, this allows for a first step of safety, so that code you have passes the static checker. It does not mean your code is perfect but can help you find warnings and errors. A great combination for this is pairing it up with drone.io, that way when a new sdk comes out drone can let you know automatically if it passes static checker.

Lets see this in action

First step is to add bot to your pubspec.yaml

1
2
3
4
dependencies:
  browser: ">=0.3.4"
  unittest: ">=0.3.4"
  bot: ">=0.12.0"

Create a minimal tool/hop_runner.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
library hop_runner;

import 'dart:async';
import 'dart:io';
import 'package:bot/bot.dart';
import 'package:bot/hop.dart';
import 'package:bot/hop_tasks.dart';

void main() {
  //
  // Analyzer
  //
  addTask('analyze_lib', createDartAnalyzerTask(['lib/stats.dart']));

  //
  // Hop away!
  //
  runHopCore();
}

Add a bin/hop script to your project, bin/hop script is not required but does help manage some flags you might be interested in having.

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
PACK_DIR=`pwd`/packages/
cmd="dart
--enable_asserts
--enable_checked_mode
--enable_type_checks
--error_on_malformed_type
--package-root=$PACK_DIR
--verbose_debug
--warning_as_error
./tool/hop_runner.dart $@"
exec $cmd

Now hop away!

1
2
3
4
5
19:41:54-adam@Adams-MacBook-Air:~/dart/stats.dart
$ hop analyze_lib
analyze_lib: PASSED - lib/stats.dart
analyze_lib: PASSED: 1, WARNING: 0, ERROR: 0
Finished

Now lets say we have the following warning of String i = 42;, bot can provide us a summary of warnings we might have in our code.

1
2
3
4
5
library not_cool_lib;

void main() {
  String i = 42;
}

Adding the library to our task

1
2
3
4
  //
  // Analyzer
  //
  addTask('analyze_lib', createDartAnalyzerTask(['lib/stats.dart', 'lib/notcoollib.dart']));

Executing hop

1
2
3
4
5
6
20:04:55-adam@Adams-MacBook-Air:~/dart/stats.dart
$ hop analyze_lib
analyze_lib: PASSED - lib/stats.dart
analyze_lib: WARNING - lib/notcoollib.dart
analyze_lib: PASSED: 1, WARNING: 1, ERROR: 0
Finished

Now we have a warning, but since dart is a dynamic langauge we should not treat that as an error (at some point we might provide the option of making warnings fail drone.io).

If we introduce a real error, the task runner should yell at us.

1
2
3
4
5
6
library not_cool_lib;

void main() {
  String i = 42;
  bam(i);
}

And it does!

1
2
3
4
5
6
7
8
9
10
20:11:38-adam@Adams-MacBook-Air:~/dart/stats.dart
$ hop analyze_lib
analyze_lib: PASSED - lib/stats.dart
analyze_lib: ERROR - lib/notcoollib.dart
analyze_lib: PASSED: 1, WARNING: 0, ERROR: 1
analyze_lib: Failed
Failed
20:18:11-adam@Adams-MacBook-Air:~/dart/stats.dart
$ echo $?
80

The exit code will let drone.io know that we did not exit cleanly. I love drone.io for this reason, it’s simple to get setup right away with little fuss.

A complete run on drone.io might look something as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ git clone git://github.com/Dartist/stats.dart.git /home/ubuntu/src/github.com/Dartist/stats.dart 
Cloning into '/home/ubuntu/src/github.com/Dartist/stats.dart'...
$ dart --version
Dart VM version: 0.3.4.0_r18115 (Tue Feb  5 05:53:42 2013)
$ cat $DART_SDK/revision
18137
$ sudo start xvfb
xvfb start/running, process 1017
$ pub install
Resolving dependencies...
Downloading browser 0.3.4...
Downloading bot 0.12.1...
Downloading unittest 0.3.4...
Downloading logging 0.3.4...
Downloading args 0.3.4...
Downloading meta 0.3.4...
Dependencies installed!
$ export PATH=./bin:$PATH
$ hop analyze_lib
analyze_lib: PASSED - lib/stats.dart
analyze_lib: PASSED: 1, WARNING: 0, ERROR: 0
Finished
$ hop headless_test
unittest-suite-wait-for-done
headless_test: DumpRenderTree - test/tests_browser.html
headless_test: 1 PASSED, 0 FAILED, 0 ERRORS
Finished

For a full example of a project that uses bot.dart for testing and analyzer please refer to stats.dart