Статьи

Руководство по возвращению правила Scala: выполнение

Это основано на первой части этого урока. В этом посте мы создадим правило для создания исполняемого файла.

Захват Вывод из scalac

В конце урока в прошлый раз мы вызывали скаляр, но игнорировали результат:

(cd /private/var/tmp/_bazel_kchodorow/92df5f72e3c78c053575a1a42537d8c3/blerg && \
  exec env - \
  /bin/bash -c 'external/scala/bin/scalac HelloWorld.scala; echo '\''blah'\'' > bazel-out/local_darwin-fastbuild/bin/hello-world.sh')

Если вы посмотрите на каталог, в котором выполняется действие ( / private / var / tmp / _bazel_kchodorow / 92df5f72e3c78c053575a1a42537d8c3 / blerg в моем случае), то увидите, что HelloWorld.class и HelloWorld $ .class созданы. Этот каталог называется корневым каталогом выполнения , именно здесь Bazel выполняет действия по сборке. Bazel использует отдельные деревья каталогов для исходного кода, выполнения действий по сборке и выходных файлов ( bazel-out / ). Файлы не будут перемещены из корневого каталога выполнения в выходное дерево, если мы не скажем Bazel, что хотим их.

Мы хотим, чтобы наша скомпилированная scala-программа заканчивалась в bazel-out / , но есть небольшое осложнение. В таких языках, как Java (и Scala), один исходный файл может содержать внутренние классы, которые вызывают генерацию нескольких файлов .class одним действием компиляции. Базель не может знать, пока не запустит действие, сколько файлов классов будет сгенерировано. Однако Bazel требует, чтобы каждое действие заранее объявляло, какими будут его результаты. Чтобы обойти это, нужно упаковать файлы .class и сделать результирующий архив выводом сборки.

В этом примере мы добавим файлы .class в .jar. Давайте добавим это к выводам, которые теперь должны выглядеть так:

  outputs = {
    'jar': "%{name}.jar",
    'sh': "%{name}.sh",
  },

В этой implфункции наша команда немного усложняется, поэтому я собираюсь изменить ее на массив команд, а затем присоединить их к «\ n» в действии:

def impl(ctx):
    cmd = [
        "%s %s" % (ctx.file._scalac.path, ctx.file.src.path),
        "find . -name '*.class' -print > classes.list",
        "jar cf %s @classes.list" % (ctx.outputs.jar.path),
    ]

    ctx.action(
        inputs = [ctx.file.src],
command = "\n".join(cmd),
        outputs = [ctx.outputs.jar]
    )

Это скомпилирует src, найдет все файлы .class и добавит их в выходной jar. Если мы запустим это, мы получим:

$ bazel build -s :hello-world
INFO: Found 1 target...
>>>>> # //:hello-world [action 'Unknown hello-world.jar']
(cd /private/var/tmp/_bazel_kchodorow/92df5f72e3c78c053575a1a42537d8c3/blerg && \
  exec env - \
  /bin/bash -c 'external/scala/bin/scalac HelloWorld.scala
find . -name '\''*.class'\'' -print > classes.list
jar cf bazel-out/local_darwin-fastbuild/bin/hello-world.jar @classes.list')
Target //:hello-world up-to-date:
  bazel-bin/hello-world.jar
INFO: Elapsed time: 4.774s, Critical Path: 4.06s

Давайте посмотрим, что содержит hello-world.jar :

$ jar tf bazel-bin/hello-world.jar
META-INF/
META-INF/MANIFEST.MF
HelloWorld$.class
HelloWorld.class

Выглядит неплохо! Однако мы не можем запустить этот jar, потому что java не знает, каким должен быть основной класс:

$ java -jar bazel-bin/hello-world.jar 
no main manifest attribute, in bazel-bin/hello-world.jar

Подобно java_binaryправилу , давайте добавим main_classатрибут scala_binaryи поместим его в манифест банки. Добавить 'main_class' : attr.string(),в scala_binary«s attrsи изменения cmdк следующему:

    cmd = [
        "%s %s" % (ctx.file._scalac.path, ctx.file.src.path),
        "echo Manifest-Version: 1.0 > MANIFEST.MF",
        "echo Main-Class: %s >> MANIFEST.MF" % ctx.attr.main_class,
        "find . -name '*.class' -print > classes.list",
"jar cfm %s MANIFEST.MF @classes.list" % (ctx.outputs.jar.path),
    ]

Не забудьте обновить фактический файл BUILD, добавив атрибут main_class:

# BUILD
load("/scala", "scala_binary")

scala_binary(
    name = "hello-world",
    src = "HelloWorld.scala",
    main_class = "HelloWorld",
)

Теперь сборка и запуск дает вам:

$ bazel build :hello-world
INFO: Found 1 target...
Target //:hello-world up-to-date:
  bazel-bin/hello-world.jar
INFO: Elapsed time: 4.663s, Critical Path: 4.05s
$ java -jar bazel-bin/hello-world.jar 
Exception in thread "main" java.lang.NoClassDefFoundError: scala/Predef$
at HelloWorld$.main(HelloWorld.scala:4)
at HelloWorld.main(HelloWorld.scala)
Caused by: java.lang.ClassNotFoundException: scala.Predef$
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 2 more

Ближе! Теперь он не может найти необходимые библиотеки Scala. Вы можете добавить его вручную в командной строке, чтобы увидеть, что наш jar действительно работает, если мы также укажем jar библиотеки scala:

$ java -cp $(bazel info output_base)/external/scala/lib/scala-library.jar:bazel-bin/hello-world.jar HelloWorld
Hello, world!

Поэтому нам нужно наше правило для генерации исполняемого файла, который в основном выполняет эту команду, что можно сделать, добавив еще одно действие в нашу сборку. Сначала мы добавим зависимость от scala-library.jar , добавив ее как скрытый атрибут:

        '_scala_lib': attr.label(
            default=Label("@scala//:lib/scala-library.jar"),
            allow_files=True,
            single_file=True),

Создание scala_binaryисполняемого файла

Давайте на минуту остановимся и переключимся: мы расскажем Базелю, что scala_binaryэто двоичные файлы. Для этого мы добавляем executable = Trueв attrs и избавляемся от ссылки на hello-world.sh в выходных данных:

...
    outputs = {
        'jar': "%{name}.jar",
    },
    implementation = impl,
    executable = True,
)

Это говорит о том, что scala_binary(name = "foo", ...)должно иметь действие, которое создает двоичный файл с именем foo, на который можно ссылаться через ctx.outputs.executableфункцию реализации. Теперь мы можем использовать bazel run :hello-world(вместо bazel build :hello-world; ./bazel-bin/hello-world.sh).

Исполняемый файл, который мы хотим создать, это команда java сверху, поэтому мы добавляем второе действие impl, это действие файла (так как мы просто генерируем файл с определенным содержимым, а не выполняем серию команд для генерации .jar ):

    cp = "%s:%s" % (ctx.outputs.jar.basename, ctx.file._scala_lib.path)
    content = [
"#!/bin/bash",
        "echo Running from $PWD",
"java -cp %s %s" % (cp, ctx.attr.main_class),
    ]
    ctx.file_action(
content = "\n".join(content),
output = ctx.outputs.executable,
    )

Обратите внимание, что я также добавил строку в файл, чтобы указать, откуда он запускается. Если мы сейчас используем bazel run, вы увидите:

$ bazel run :hello-world
INFO: Found 1 target...
Target //:hello-world up-to-date:
  bazel-bin/hello-world.jar
  bazel-bin/hello-world
INFO: Elapsed time: 2.694s, Critical Path: 0.08s

INFO: Running command line: bazel-bin/hello-world
Running from /private/var/tmp/_bazel_kchodorow/92df5f72e3c78c053575a1a42537d8c3/blerg/bazel-out/local_darwin-fastbuild/bin/hello-world.runfiles
Error: Could not find or load main class HelloWorld
ERROR: Non-zero return code '1' from command: Process exited with status 1.

К сожалению, он не может найти банки! И что это за путь, hello-world.runfiles , из которого он запускает бинарный файл?

Каталог runfiles

bazel runзапускает двоичный файл из каталога runfiles , каталога, отличного от исходного корня, корневого каталога выполнения и выходного дерева, упомянутых выше. Каталог runfiles должен содержать все ресурсы, необходимые исполняемому файлу во время выполнения. Обратите внимание, что это не корень выполнения , который используется во время bazel buildшага. Когда вы действительно выполняете что-то, созданное bazel, его ресурсы должны находиться в каталоге runfiles .

В этом случае нашему исполняемому файлу необходим доступ к hello-world.jar и scala-library.jar . Чтобы добавить эти файлы, API несколько странно. Вы должны вернуть структуру, содержащую объект runfiles, из реализации правила. Таким образом, добавьте следующее как последнюю строку вашей implфункции:

return struct(runfiles = ctx.runfiles(files = [ctx.outputs.jar, ctx.file._scala_lib]))

Теперь, если вы запустите его снова, он напечатает:

$ bazel run :hello-world
INFO: Found 1 target...
Target //:hello-world up-to-date:
  bazel-bin/hello-world.jar
  bazel-bin/hello-world
INFO: Elapsed time: 0.416s, Critical Path: 0.00s

INFO: Running command line: bazel-bin/hello-world
Running from /private/var/tmp/_bazel_kchodorow/92df5f72e3c78c053575a1a42537d8c3/blerg/bazel-out/local_darwin-fastbuild/bin/hello-world.runfiles
Hello, world!

Ура!

Однако! Если мы запустим его как bazel-bin / hello-world , он не сможет найти файлы jar (потому что мы не в каталоге runfiles). Чтобы найти каталог runfiles независимо от того, откуда запускается двоичный файл, измените свою contentпеременную на следующую:

    content = [
        "#!/bin/bash",
        "case \"$0\" in",
        "/*) self=\"$0\" ;;",
        "*)  self=\"$PWD/$0\";;",
        "esac",
        "(cd $self.runfiles; java -cp %s %s)" % (cp, ctx.attr.main_class),
    ]

Таким образом, если он запускается из bazel run, $0это будет абсолютный путь к бинарному файлу (в моем случае это / private / var / tmp / _bazel_kchodorow / 92df5f72e3c78c053575a1a42537d8c3 / blerg / bazel-out / local_darwin-fastbuild / bin / hello-world ). Если он запускается через bazel-bin / hello-world , $0будет просто так: bazel-bin / hello-world . В любом случае мы окажемся в каталоге runfiles перед выполнением команды.

Теперь наше правило успешно генерирует двоичный файл. Вы можете увидеть полный код этого примера на GitHub .

В заключительной части этого урока мы исправим оставшиеся проблемы:

  • Нет поддержки нескольких исходных файлов, не говоря уже о зависимостях.
  • [action 'Unknown hello-world.jar'] довольно некрасиво

До скорого!