asseticでlessFileterを使いたかった

windowsで使おうとすると、ちょっと工夫が必要だったというか出来なかった。つらみ( ´_ゝ`)
長いのに中身が無いので結論を先に読んで察してから中を読むことをお勧めします。*1

asseticに関しては前回書いたので良ければそっちを見てください。

asseticではlessphpのフィルタなども用意されてるので、そっちを素直に使えばいいんですが、CoffeeScriptphpFilterは無いし*2、たぶん同じ原因でCoffeeScriptFilter落ちてるので頑張ってみました。

一応コードはhttps://github.com/matsu-chara/assetic_testにあります。

環境

  • win7
  • assetic 1.2
  • node 0.10.17
  • npm 1.3.8

lessの準備

  1. node.jsからnode.jsをインストール。同時にnpmもインストールされます。
  1. npmでグローバルインストール(-g 付きでインストール)したときにnode側で読み込めるように環境変数NODE_PATHを設定します。*3
>setx NODE_PATH %APPDATA%\npm\node_modules

win7以外だとnode_modulesの場所が違うかもしれません。

  1. 続いてlessをインストール
>npm install -g less

ここまででコマンドプロンプト上で適当なlessファイル

/* a.less */
@color: #00ff00;

#zannen-red {
  background-color:red;
  p {
    background-color:@color;
  }
}

に対してlesscでコンパイルするとcssになって下記のように出力されるはずです。

>lessc a.less

/* a.less */
#zannen-red {
  background-color: red;
}
#zannen-red p {
  background-color: #00ff00;
}

lessFilterの準備

公式サンプルを見ながら下記のようなPHPファイルassetic_less.phpを用意します。

<?php
require '../vendor/autoload.php';

use Assetic\Asset\AssetCollection;
use Assetic\Asset\FileAsset;
use Assetic\Filter\LessFilter;

$css = new Asset Collection (array (
    new FileAsset('css/a.less', array(new LessFilter())),
    new FileAsset('css/b.css'),
));

// header('Content-Type: text/css');
echo $css->dump();


あとはhtmlファイルのlinkでassetic_less.phpを読み込ませてやればOK・・・となるはずでした。

Assetic\Exception\FilterException: in C:\xampp\htdocs\assetic_test\vendor\kriswallsmith\assetic\src\Assetic\Exception\FilterException.php on line 40

ということで、原因を探した。

# 原因その1

vendor\kriswallsmith\assetic\src\Assetic\FilterのLessFilter.phpにあるLessFilterクラスのコンストラクタで引数に$nodeBinが必要らしい。デフォルトだと/usr/bin/nodeなのでwindowsでは渡さないと動くはずもなかった・・・。
ということでLessFilterのインスタンス作成時にnode.exeへのパスを渡してやる。

<?php
require '../vendor/autoload.php';

use Assetic\Asset\AssetCollection;
use Assetic\Asset\FileAsset;
use Assetic\Filter\LessFilter;
$nodepath = 'C:\Program Files\nodejs\node.exe';

$css = new AssetCollection(array(
    new FileAsset('css/a.less', array(new LessFilter($nodepath))),
    new FileAsset('css/b.css'),
));

// header('Content-Type: text/css');
echo $css->dump();
Assetic\Exception\FilterException: An error occurred while running: "C:\Program Files\nodejs\node.exe" "C:\Windows\Temp\assC737.tmp" Error Output: module.js:340 throw err; ^ Error: Cannot find module 'less' at Function.Module._resolveFilename (module.js:338:15) at Function.Module._load (module.js:280:25) at Module.require (module.js:364:17) at require (module.js:380:17) at Object.<anonymous> (C:\Windows\Temp\assC737.tmp:1:74) at Module._compile (module.js:456:26) at Object.Module._extensions..js (module.js:474:10) at Module.load (module.js:356:32) at Function.Module._load (module.js:312:12) at Function.Module.runMain (module.js:497:10) Input: /* a.less */ @color: #00ff00; #zannen-red { background-color:red; p { background-color:@color; } } in C:\xampp\htdocs\assetic_test\vendor\kriswallsmith\assetic\src\Assetic\Exception\FilterException.php on line 40
Call Stack

原因その2

こことかここが問題のよう

とりあえずnodepathが読み込めて居ない様子。対処法は二つで

  • 下記のようにnodepathの配列をLessFilterを作成する際に渡す。

のどちらかを行えばいいらしい。

<?php
require '../vendor/autoload.php';

use Assetic\Asset\AssetCollection;
use Assetic\Asset\FileAsset;
use Assetic\Filter\LessFilter;

$nodebin = 'C:\Program Files\nodejs\node.exe';
$nodepaths = array('{{AppDataへのパス}}\Roaming\npm\node_modules');

$css = new AssetCollection(array(
    new FileAsset('css/a.less', array(new LessFilter($nodebin, $nodepaths))),
    new FileAsset('css/b.css'),
));

// header('Content-Type: text/css');
echo $css->dump();

これでエラーはなくなるけどb.cssしか出力されない・・。。

原因その3

symfony process builderとcmdの間で出力が共有されてないために
nodeが正常に実行されていても$proc->getOutput()がnullになってしまうのが原因のよう。
issueでsystemroot環境変数が渡されていないせいと指摘されているけど既にcommitがmergeされている模様
(BaseProcessFilterのmergeEnv関連)

\kriswallsmith\assetic\src\Assetic\Filter\LessFilter.php
で作成されている$procをvar_dumpすると確認できる

>cmd /V:ON /E:ON /C ""C:\Program Files\nodejs\node.exe" "C:\Users\<ユーザ名>\AppData\Local\Temp\ass815C.tmp""

コマンドプロンプトで直接叩くと正常に動作する模様*5

ということでAsseticじゃなくてSymfonyProcessBuilderを見てみました。
C:\xampp\htdocs\assetic_test\vendor\symfony\process\Symfony\Component\Process\Process.php
あたりの$descriptorsを見てみると

var_dump($descriptors);
↓
array (size=3)
  0 => 
    array (size=2)
      0 => string 'pipe' (length=4)
      1 => string 'r' (length=1)
  1 => resource(29, stream)
  2 => 
    array (size=2)
      0 => string 'pipe' (length=4)
      1 => string 'w' (length=1)

となんだか見慣れない形。
ためしに

$descriptors = array(
    0 => array("pipe","r"),
    1 => array("pipe","w")
    );

にしたら動いた!第3部完!

と思ったけどgetDescriptors()のコメント読むと

//Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big.
//Workaround for this problem is to use temporary files instead of pipes on Windows platform.
//@see https://bugs.php.net/bug.php?id=51800

と書いてあった。proc_openで2000bytes以上かませるとハングするらしい。
bootstrap.minが104KBだったので余裕でアウトな気がします。

If you change the script below to not include the STDERR descriptor or if you change the STDERR descriptor to a file output everything will work fine. Also if you close the STDERR pipe before reading from STDOUT it will work. There seems to be some deadlock.

らしいのでサンプルバグらせコードのSTDERRを消したバージョンを用意した。
*6

大丈夫みたい。一応手元で$dataを100*1024*1024にしたけど動作した。(時間かかるけど)なので
C:\xampp\htdocs\assetic_test\vendor\symfony\process\Symfony\Component\Process\Process.phpの968行目付近を

- return array(array('pipe', 'r'), $this->fileHandles[self::STDOUT], array('pipe', 'w'));

+ //return array(array('pipe', 'r'), $this->fileHandles[self::STDOUT], array('pipe', 'w'));
+ return array(array('pipe', 'r'), array('pipe', 'w'));

とした。動いた!*7

でもstderrに書き込もうとしたら落ちそう(未確認)。というかエラー起きた時に何だかわからなくなるのは厳しい・・・。symfony process builderのアップデート待ちというところでしょうか。

結論

AsseticのLessFilter使いたいときはLessphpFilter使うかOSを変えよう!

*1:じゃあ書くなよ感 本当は出来るようになってハッピーな記事になる予定だったんだよ(´;ω;`)

*2:coffeePHP自体は有志によって作成されている模様

*3:ローカルインストールで構わない場合はこの手順は不要です。

*4:setxするとユーザー環境変数に登録される。setx -mを使えばシステム環境変数に登録できる。

*5:全然関係ないけど, tempnamって初めて知った。パーミッションも設定してくれるらしい。便利だ。

*6:いまさらgistsのコード貼れることに気がついた。

*7:CoffeeScriptFilterもこの修正で動作した。