Ngôn ngữ và Vận hành GN

GN là một hệ thống xây dựng cấu hình có cú pháp đơn giản và mạnh mẽ. Bài viết này đi sâu vào các khía cạnh ngôn ngữ và cách thức hoạt động của GN.

Trợ giúp tích hợp sẵn

GN cung cấp một hệ thống trợ giúp toàn diện. Để xem trợ giúp về mọi hàm và biến dựng sẵn, hãy chạy lệnh:

gn help

Ngoài ra, bạn có thể tham khảo tài liệu giới thiệu về GN để hiểu rõ hơn về triết lý thiết kế và các khái niệm cốt lõi.

Triết lý thiết kế

  • Việc viết tệp cấu hình xây dựng không nên là một công việc sáng tạo. Cần hạn chế tối đa sự linh hoạt không cần thiết và coi các lỗi phổ biến là lỗi nghiêm trọng.
  • Ngôn ngữ định nghĩa nên giống mã nguồn hơn là các quy tắc. Mục tiêu là sử dụng các ngôn ngữ quen thuộc như C++ và Python, thay vì các ngôn ngữ như Prolog.
  • Ngôn ngữ xây dựng có quan điểm rõ ràng về cách thức hoạt động. Nó không nhất thiết phải dễ dàng hoặc có thể biểu diễn mọi thứ tùy ý. Mục tiêu là điều chỉnh mã nguồn và công cụ để đơn giản hóa quá trình xây dựng.
  • Tham khảo Blaze khi phù hợp.

Ngôn ngữ

GN sử dụng một ngôn ngữ có kiểu động, cực kỳ đơn giản với các kiểu dữ liệu sau:

  • Boolean (true, false).
  • Số nguyên 64 bit có dấu.
  • Chuỗi ký tự (String).
  • Danh sách (List) chứa các kiểu dữ liệu khác.
  • Phạm vi (Scope), tương tự như một từ điển cho các mục dựng sẵn.

Có một số biến dựng sẵn có giá trị phụ thuộc vào môi trường hiện tại. Xem gn help để biết thêm chi tiết.

Ngôn ngữ có nhiều điểm thiếu sót có chủ ý. Ví dụ, không có lệnh gọi hàm do người dùng định nghĩa (template là thứ gần nhất). Theo triết lý thiết kế, nếu bạn cảm thấy cần những thứ này, có thể bạn đang làm sai cách.

Ngữ pháp đầy đủ dành cho những người quan tâm đến chi tiết có sẵn trong gn help grammar.

Chuỗi ký tự (Strings)

Chuỗi được bao quanh bởi dấu ngoặc kép và sử dụng ký tự gạch chéo ngược (\) làm ký tự thoát. Các chuỗi thoát được hỗ trợ bao gồm:

  • \" (cho dấu ngoặc kép thực tế)
  • \$ (cho ký tự đô la thực tế)
  • \\ (cho ký tự gạch chéo ngược thực tế)

Bất kỳ cách sử dụng gạch chéo ngược nào khác đều được coi là ký tự gạch chéo ngược thực tế. Ví dụ, \b trong các mẫu đường dẫn không cần thoát, cũng như nhiều đường dẫn Windows như "C:\foo\bar.h".

Tính năng thay thế biến đơn giản được hỗ trợ thông qua ký tự $, trong đó từ theo sau ký tự đô la sẽ được thay thế bằng giá trị của biến. Bạn có thể tùy chọn bao quanh tên biến bằng dấu ngoặc nhọn {} nếu không có ký tự không phải tên biến nào kết thúc tên biến. Không hỗ trợ các biểu thức phức tạp hơn, chỉ thay thế tên biến.

a = "mypath"
b = "$a/foo.cc"  # b sẽ là "mypath/foo.cc"
c = "foo${a}bar.cc"  # c sẽ là "foomypathbar.cc"

Bạn có thể mã hóa các ký tự 8 bit bằng cú pháp "$0xFF". Ví dụ, một chuỗi có ký tự xuống dòng (0x0A) sẽ trở thành "look$0x0Alike$0x0Athis".

Danh sách (Lists)

Ngoài việc phân biệt danh sách rỗng và không rỗng (a == []), không có cách nào để lấy độ dài của một danh sách. Nếu bạn cần làm điều này, có thể bạn đang thực hiện quá nhiều công việc trong quá trình xây dựng.

Danh sách hỗ trợ nối thêm:

a = [ "first" ]
a += [ "second" ]  # a trở thành [ "first", "second" ]
a += [ "third", "fourth" ]  # a trở thành [ "first", "second", "third", "fourth" ]
b = a + [ "fifth" ]  # b trở thành [ "first", "second", "third", "fourth", "fifth" ]

Nối một danh sách vào danh sách khác sẽ nối các phần tử của danh sách thứ hai, thay vì nối danh sách như một thành viên lồng nhau.

Bạn có thể xóa các phần tử khỏi danh sách:

a = [ "first", "second", "third", "first" ]
b = a - [ "first" ]  # b trở thành [ "second", "third" ]
a -= [ "second" ]  # a trở thành [ "first", "third", "first" ]

Toán tử - trên một danh sách sẽ tìm kiếm các phần tử khớp và xóa tất cả các phần tử đó. Trừ một danh sách từ danh sách khác sẽ xóa từng phần tử trong danh sách thứ hai.

Nếu không tìm thấy phần tử khớp nào, một lỗi sẽ được đưa ra. Do đó, bạn cần biết trước rằng phần tử đó tồn tại trước khi xóa. Vì không có cách nào để kiểm tra sự tồn tại, trường hợp sử dụng chính là thiết lập một danh sách chính các tệp hoặc cờ, sau đó xóa những cái không áp dụng cho quá trình xây dựng hiện tại dựa trên các điều kiện khác nhau.

Về mặt phong cách, nên ưu tiên chỉ thêm vào danh sách và đảm bảo mỗi tệp nguồn hoặc phụ thuộc xuất hiện một lần. Điều này trái ngược với lời khuyên trước đây của nhóm Chrome dành cho GYP (GYP ưu tiên liệt kê tất cả các tệp, sau đó loại bỏ những tệp không mong muốn trong các khối điều kiện).

Danh sách hỗ trợ chỉ số hóa bắt đầu từ 0 để trích xuất giá trị:

a = [ "first", "second", "third" ]
b = a[1]  # b sẽ là "second"

Toán tử [] chỉ đọc và không thể dùng để thay đổi danh sách. Trường hợp sử dụng chính là khi một tập lệnh bên ngoài trả về nhiều giá trị đã biết và bạn muốn trích xuất chúng.

Có một số trường hợp dễ dàng ghi đè một danh sách khi bạn muốn nối thêm vào nó. Để giúp phát hiện trường hợp này, việc gán một danh sách không rỗng cho một biến chứa một danh sách không rỗng hiện có sẽ gây lỗi. Nếu bạn muốn bỏ qua hạn chế này, hãy gán biến đích bằng danh sách rỗng trước.

a = [ "one" ]
a = [ "two" ]  # Lỗi: ghi đè danh sách không rỗng bằng danh sách không rỗng.
a = []         # OK
a = [ "two" ]  # OK

Lưu ý rằng quá trình thực thi tập lệnh xây dựng được thực hiện mà không có kiến thức nội tại về ý nghĩa của dữ liệu cơ bản. Điều này có nghĩa là nó không biết rằng sources là một danh sách tên tệp, ví dụ. Do đó, nếu bạn xóa một phần tử, nó phải khớp với chuỗi ký tự thực tế thay vì chỉ định một tên khác sẽ phân giải thành cùng một tên tệp.

Điều kiện (Conditionals)

Các câu lệnh điều kiện có cú pháp giống C:

  if (is_linux || (is_win && target_cpu == "x86")) {
    sources -= [ "something.cc" ]
  } else if (...) {
    ...
  } else {
    ...
  }

Bạn có thể sử dụng chúng ở hầu hết mọi nơi, ngay cả xung quanh toàn bộ các mục tiêu (targets) nếu mục tiêu đó chỉ nên được khai báo trong một số trường hợp nhất định.

Vòng lặp (Looping)

Bạn có thể lặp qua một danh sách bằng foreach. Tuy nhiên, việc này không được khuyến khích. Hầu hết các công việc mà hệ thống xây dựng cần thực hiện có thể được biểu diễn mà không cần sử dụng vòng lặp. Nếu bạn thấy nó cần thiết, đó có thể là dấu hiệu cho thấy bạn đang thực hiện quá nhiều công việc trong giai đoạn xây dựng meta.

foreach(i, mylist) {
  print(i)  # Lưu ý: i là bản sao của mỗi phần tử, không phải tham chiếu đến nó.
}

Gọi hàm (Function calls)

Các lệnh gọi hàm đơn giản có cú pháp giống hầu hết các ngôn ngữ khác:

print("hello, world")
assert(is_win, "This should only be executed on Windows")

Các hàm như vậy là dựng sẵn và người dùng không thể định nghĩa hàm mới.

Một số hàm nhận một khối mã được bao quanh bởi dấu ngoặc nhọn {} theo sau chúng:

static_library("mylibrary") {
  sources = [ "a.cc" ]
}

Hầu hết các hàm này định nghĩa các mục tiêu. Người dùng có thể định nghĩa các hàm mới bằng cơ chế template được thảo luận bên dưới.

Chính xác hơn, biểu thức này có nghĩa là khối mã đó trở thành một đối số cho hàm để hàm đó thực thi. Hầu hết các hàm sử dụng khối mã sẽ thực thi khối đó và coi phạm vi kết quả như một từ điển các biến để đọc.

Phạm vi và Thực thi (Scoping and execution)

Các tệp và lệnh gọi hàm theo sau bởi các khối {} sẽ giới thiệu phạm vi mới. Các phạm vi được lồng nhau. Khi bạn đọc một biến, các phạm vi chứa sẽ được tìm kiếm theo thứ tự đảo ngược cho đến khi tìm thấy tên khớp. Gán biến luôn diễn ra trong phạm vi trong cùng.

Không có cách nào để sửa đổi bất kỳ phạm vi nào bên ngoài phạm vi trong cùng. Điều này có nghĩa là khi bạn định nghĩa một mục tiêu, ví dụ, không có gì bạn làm bên trong khối sẽ "rò rỉ" ra phần còn lại của tệp.

Các câu lệnh if/else/foreach, mặc dù sử dụng {}, không giới thiệu phạm vi mới, do đó các thay đổi sẽ tồn tại bên ngoài câu lệnh.

Đặt tên

Tên tệp và thư mục

Tên tệp và thư mục là các chuỗi ký tự và được diễn giải là tương đối so với thư mục của tệp cấu hình hiện tại. Có ba dạng có thể:

Tên tương đối:

"foo.cc"
"src/foo.cc"
"../src/foo.cc"

Tên tuyệt đối trong cây nguồn:

"//net/foo.cc"
"//base/test/foo.cc"

Tên tuyệt đối của hệ thống (hiếm, thường dùng cho thư mục bao gồm):

"/usr/local/include/"
"/C:/Program Files/Windows Kits/Include"

Cấu hình xây dựng

Mục tiêu (Targets)

Mục tiêu là một nút trong đồ thị xây dựng. Nó thường đại diện cho một loại tệp thực thi hoặc thư viện sẽ được tạo ra. Các mục tiêu phụ thuộc vào các mục tiêu khác. Các loại mục tiêu dựng sẵn (xem gn help <targettype> để biết thêm trợ giúp) bao gồm:

  • action: Chạy một tập lệnh để tạo tệp.
  • action_foreach: Chạy một tập lệnh một lần cho mỗi tệp nguồn.
  • bundle_data: Khai báo dữ liệu để đưa vào gói ứng dụng trên Mac/iOS.
  • create_bundle: Tạo gói ứng dụng trên Mac/iOS.
  • executable: Tạo một tệp thực thi.
  • group: Một nút phụ thuộc ảo tham chiếu đến một hoặc nhiều mục tiêu khác.
  • shared_library: Một tệp .dll hoặc .so.
  • loadable_module: Một tệp .dll hoặc .so chỉ được tải khi chạy.
  • source_set: Một thư viện tĩnh ảo nhẹ (thường được ưu tiên hơn thư viện tĩnh thực tế vì nó sẽ biên dịch nhanh hơn).
  • static_library: Một tệp .lib hoặc .a (thường bạn sẽ muốn source_set thay thế).

Bạn có thể mở rộng điều này để tạo các loại mục tiêu tùy chỉnh bằng cách sử dụng template (xem bên dưới). Trong Chrome, một số template được sử dụng phổ biến bao gồm:

  • component: Là source set hoặc shared library, tùy thuộc vào loại xây dựng.
  • test: Một tệp thực thi kiểm thử. Trên thiết bị di động, nó sẽ tạo loại ứng dụng gốc phù hợp cho các bài kiểm thử.
  • app: Tệp thực thi hoặc ứng dụng Mac/iOS.
  • android_apk: Tạo tệp APK. Có rất nhiều loại khác cho Android, xem //build/config/android/rules.gni.

Cấu hình (Configs)

Config là các đối tượng có tên xác định một tập hợp các cờ (flags), thư mục bao gồm (include directories) và định nghĩa (defines). Chúng có thể được áp dụng cho một mục tiêu và được đẩy tới các mục tiêu phụ thuộc.

Để định nghĩa một config:

config("myconfig") {
  includes = [ "src/include" ]
  defines = [ "ENABLE_DOOM_MELON" ]
}

Để áp dụng một config cho một mục tiêu:

executable("doom_melon") {
  configs = [ ":myconfig" ]
}

Thông thường, tệp cấu hình xây dựng sẽ chỉ định các mặc định của mục tiêu (target defaults) đặt một danh sách mặc định các configs. Các mục tiêu có thể thêm hoặc bớt vào danh sách này khi cần. Vì vậy, trong thực tế, bạn thường sẽ sử dụng configs += ":myconfig" để nối thêm vào danh sách mặc định.

Xem gn help config để biết thêm thông tin về cách khai báo và áp dụng configs.

Cấu hình công khai (Public Configs)

Một mục tiêu có thể áp dụng các cài đặt cho các mục tiêu khác phụ thuộc vào nó. Ví dụ phổ biến nhất là một mục tiêu của bên thứ ba yêu cầu một số định nghĩa hoặc thư mục bao gồm để các tiêu đề của nó biên dịch đúng cách. Bạn muốn các cài đặt này áp dụng cho cả quá trình biên dịch thư viện bên thứ ba cũng như tất cả các mục tiêu sử dụng thư viện đó.

Để thực hiện điều này, bạn viết một config với các cài đặt bạn muốn áp dụng:

config("my_external_library_config") {
  includes = "."
  defines = [ "DISABLE_JANK" ]
}

Sau đó, config này được thêm vào mục tiêu dưới dạng public. Nó sẽ áp dụng cho cả mục tiêu cũng như các mục tiêu phụ thuộc trực tiếp vào nó.

shared_library("my_external_library") {
  ...
  # Các mục tiêu phụ thuộc vào cái này sẽ được áp dụng config này.
  public_configs = [ ":my_external_library_config" ]
}

Các mục tiêu phụ thuộc lần lượt có thể chuyển tiếp điều này lên một cấp khác trong cây phụ thuộc bằng cách thêm mục tiêu của bạn dưới dạng phụ thuộc public.

static_library("intermediate_library") {
  ...
  # Các mục tiêu phụ thuộc vào mục tiêu này cũng sẽ nhận được config từ "my external library".
  public_deps = [ ":my_external_library" ]
}

Một mục tiêu có thể chuyển tiếp một config tới tất cả các phần phụ thuộc cho đến khi đạt đến ranh giới liên kết (link boundary) bằng cách đặt nó làm all_dependent_config. Điều này rất không được khuyến khích vì nó có thể làm tràn các cờ và định nghĩa ra nhiều phần của quá trình xây dựng hơn mức cần thiết. Thay vào đó, hãy sử dụng public_deps để kiểm soát cờ nào được áp dụng ở đâu.

Trong Chrome, ưu tiên hệ thống tệp tiêu đề cờ xây dựng (build/buildflag_header.gni) cho các định nghĩa, điều này ngăn chặn hầu hết các lỗi với định nghĩa trình biên dịch.

Templates

Template là cách chính của GN để tái sử dụng mã. Thông thường, một template sẽ mở rộng thành một hoặc nhiều loại mục tiêu khác.

# Khai báo một tập lệnh biên dịch các tệp IDL thành mã nguồn, sau đó biên dịch các tệp mã nguồn đó.
template("idl") {
  # Luôn dựa các mục tiêu trợ giúp vào target_name để chúng là duy nhất. Target_name
  # sẽ là chuỗi được truyền dưới dạng tên khi template được gọi.
  idl_target_name = "${target_name}_generate"
  action_foreach(idl_target_name) {
    ...
  }

  # Template của bạn luôn nên định nghĩa một mục tiêu có tên target_name.
  # Khi các mục tiêu khác phụ thuộc vào lệnh gọi template của bạn, đây sẽ là
  # đích của sự phụ thuộc đó.
  source_set(target_name) {
    ...
    deps = [ ":$idl_target_name" ]  # Yêu cầu các nguồn phải được biên dịch.
  }
}

Thông thường, định nghĩa template của bạn sẽ nằm trong tệp .gni và người dùng sẽ nhập tệp đó để xem định nghĩa template:

import("//tools/idl_compiler.gni")

idl("my_interfaces") {
  sources = [ "a.idl", "b.idl" ]
}

Việc khai báo một template tạo ra một closure xung quanh các biến trong phạm vi tại thời điểm đó. Khi template được gọi, biến ma thuật invoker được sử dụng để đọc các biến từ phạm vi gọi nó. Template thường sẽ sao chép các giá trị mà nó quan tâm vào phạm vi của riêng nó:

template("idl") {
  source_set(target_name) {
    sources = invoker.sources
  }
}

Thư mục hiện tại khi một template thực thi sẽ là thư mục của tệp cấu hình gọi nó, thay vì tệp nguồn của template. Điều này là để các tệp được truyền từ người gọi template sẽ đúng (điều này thường chiếm phần lớn việc xử lý tệp trong một template). Tuy nhiên, nếu template có các tệp riêng (có lẽ nó tạo ra một action chạy một tập lệnh), bạn sẽ muốn sử dụng các đường dẫn tuyệt đối ("//foo/...") để tham chiếu đến các tệp này để tính đến thực tế là thư mục hiện tại sẽ không thể đoán trước trong quá trình gọi. Xem gn help template để biết thêm thông tin và các ví dụ đầy đủ hơn.

Các tính năng khác

Nhập (Imports)

Bạn có thể nhập các tệp .gni vào phạm vi hiện tại bằng hàm import. Đây *không* phải là một câu lệnh include theo nghĩa của C++. Tệp được nhập được thực thi độc lập và phạm vi kết quả được sao chép vào tệp hiện tại (C++ thực thi tệp được bao gồm trong ngữ cảnh hiện tại của chỉ thị include xuất hiện). Điều này cho phép kết quả của việc nhập được lưu trữ trong bộ nhớ cache, đồng thời ngăn chặn một số cách sử dụng "sáng tạo" hơn của các câu lệnh include như các tệp được bao gồm nhiều lần.

Thông thường, một tệp .gni sẽ định nghĩa các đối số xây dựng và templates. Xem gn help import để biết thêm.

Tệp .gni của bạn có thể định nghĩa các biến tạm thời không được xuất ra các tệp nhập nó bằng cách sử dụng dấu gạch dưới đứng trước trong tên, ví dụ: _this.

Xử lý đường dẫn (Path processing)

Thông thường, bạn sẽ muốn làm cho tên tệp hoặc danh sách tên tệp trở nên tương đối với một thư mục khác. Điều này đặc biệt phổ biến khi chạy các tập lệnh, được thực thi với thư mục đầu ra xây dựng làm thư mục hiện tại, trong khi các tệp cấu hình thường tham chiếu đến các tệp tương đối so với thư mục chứa chúng.

Bạn có thể sử dụng rebase_path để chuyển đổi thư mục. Xem gn help rebase_path để biết thêm trợ giúp và ví dụ. Cách sử dụng điển hình để chuyển đổi tên tệp tương đối với thư mục hiện tại thành tương đối với thư mục gốc xây dựng là: new_paths = rebase_path("myfile.c", root_build_dir)

Mẫu (Patterns)

Patterns được sử dụng để tạo tên tệp đầu ra cho một tập hợp các đầu vào cho các loại mục tiêu tùy chỉnh, và để tự động xóa các tệp khỏi các giá trị danh sách (xem gn help filter_includegn help filter_exclude).

Chúng giống như các biểu thức chính quy đơn giản. Xem gn help label_pattern để biết thêm.

Thực thi tập lệnh (Executing scripts)

Có hai cách để thực thi tập lệnh. Tất cả các tập lệnh bên ngoài trong GN đều bằng Python. Cách đầu tiên là như một bước xây dựng. Một tập lệnh như vậy sẽ nhận đầu vào và tạo đầu ra như một phần của quá trình xây dựng. Các mục tiêu gọi tập lệnh được khai báo bằng loại mục tiêu "action" (xem gn help action).

Cách thứ hai để thực thi tập lệnh là đồng bộ trong quá trình thực thi tệp cấu hình. Điều này là cần thiết trong một số trường hợp để xác định tập hợp các tệp cần biên dịch, hoặc để lấy một số cấu hình hệ thống mà tệp cấu hình có thể phụ thuộc vào. Tệp cấu hình có thể đọc đầu ra chuẩn (stdout) của tập lệnh và hành động dựa trên nó theo nhiều cách khác nhau.

Thực thi tập lệnh đồng bộ được thực hiện bằng hàm exec_script (xem gn help exec_script để biết chi tiết và ví dụ). Vì việc thực thi tập lệnh đồng bộ yêu cầu quá trình thực thi tệp cấu hình hiện tại phải tạm dừng cho đến khi quy trình Python hoàn thành, việc dựa vào các tập lệnh bên ngoài sẽ chậm và nên được giảm thiểu.

Để ngăn chặn việc lạm dụng, các tệp được phép gọi exec_script có thể được đưa vào danh sách cho phép trong tệp .gn cấp cao nhất. Chrome thực hiện điều này để yêu cầu xem xét mã bổ sung cho các bổ sung như vậy. Xem gn help dotfile.

Bạn có thể đọc và ghi tệp một cách đồng bộ, mặc dù điều này không được khuyến khích nhưng đôi khi cần thiết khi chạy tập lệnh đồng bộ. Trường hợp sử dụng điển hình là truyền danh sách tên tệp dài hơn giới hạn dòng lệnh của nền tảng hiện tại. Xem gn help read_filegn help write_file để biết cách đọc và ghi tệp. Các hàm này nên tránh bằng mọi giá.

Các action vượt quá giới hạn độ dài dòng lệnh có thể sử dụng tệp phản hồi (response files) để khắc phục hạn chế này mà không cần ghi tệp đồng bộ. Xem gn help response_file_contents.

So sánh và khác biệt với Blaze

Blaze là hệ thống xây dựng nội bộ của Google, hiện đã được phát hành công khai dưới dạng Bazel. Nó đã truyền cảm hứng cho một số hệ thống khác như Pants và Buck.

Trong môi trường đồng nhất của Google, nhu cầu về các câu lệnh điều kiện là rất thấp và họ có thể sử dụng một vài thủ thuật (abi_deps). Chrome sử dụng các câu lệnh điều kiện ở khắp mọi nơi và nhu cầu bổ sung chúng là lý do chính khiến các tệp trông khác nhau.

GN cũng bổ sung khái niệm "configs" để quản lý một số vấn đề phụ thuộc và cấu hình phức tạp hơn, những vấn đề tương tự không phát sinh trên máy chủ. Blaze có khái niệm "configuration" giống như một toolchain của GN, nhưng được tích hợp vào chính công cụ. Cách thức hoạt động của toolchains trong GN là kết quả của việc cố gắng tách rời khái niệm này vào các tệp cấu hình một cách rõ ràng.

GN giữ lại một số khái niệm của GYP như cài đặt "all dependent" hoạt động hơi khác một chút trong Blaze. Điều này một phần là để giúp việc chuyển đổi từ mã GYP hiện có dễ dàng hơn, và các cấu trúc của GYP thường cung cấp khả năng kiểm soát chi tiết hơn (điều này có thể tốt hoặc xấu, tùy thuộc vào tình huống).

GN cũng sử dụng tên của GYP như "sources" thay vì "srcs" vì việc rút gọn như vậy có vẻ không cần thiết, mặc dù nó sử dụng "deps" của Blaze vì "dependencies" quá khó gõ. Chromium cũng biên dịch nhiều ngôn ngữ trong một mục tiêu, do đó việc chỉ định loại ngôn ngữ trên tiền tố tên mục tiêu đã bị loại bỏ (ví dụ: khỏi cc_library).

Thẻ: gn build system Configuration language gni

Đăng vào ngày 5 tháng 6 lúc 20:45