OberonCore
https://forum.oberoncore.ru/

Автоматизация сборки БлэкБокс
https://forum.oberoncore.ru/viewtopic.php?f=114&t=5176
Страница 1 из 1

Автор:  Роман М. [ Среда, 16 Июль, 2014 19:16 ]
Заголовок сообщения:  Автоматизация сборки БлэкБокс

Моё представление автоматической сборки сводится к следующему:
  1. скомпилировать все модули текущей версией компилятора
  2. прогнать тесты, выявив ошибки
  3. в случае неудачи записать всё в журнал для дальнейшего анализа и доложить о результатах
  4. если все тесты пройдены успешно
    1. собрать новую версию
    2. подготовить документацию
    3. подготовить установщиком новую версию
    4. доложить об успехе

Имеем консольную версию компилятора для Windows, Linux, BSD. Требуется лишь разработать сценарий сборки.

Автор:  Роман М. [ Четверг, 17 Июль, 2014 13:51 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

я писал(а):
Опять же, на веб-сервере не нужно выполнять процесс автоматизированной сборки. У веб-сервера другие задачи. Тем более, он ограничен по ресурсам.
Для этого я бы создал инфраструктуру на машине разработчика или специальном CI сервере.

И ещё ссылка: Continuous Integration для самых маленьких

Автор:  Иван Денисов [ Понедельник, 21 Июль, 2014 18:23 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Вот, таки настроил автосборку установочников!

Вот тут лежат результаты:
http://blackboxframework.org/dev

Тут больше деталей:
http://redmine.blackboxframework.org/issues/3#note-6


Получился вот такой файл примерно, это некий начальный вариант, который предполагается улучшать. Ваши предложения приветствуются!

Код:
#!/usr/bin/python

from subprocess import Popen, PIPE
import sys, datetime

# this files contains hashes of last commits in master and development branches
DEVHASH = open("/var/www/tribiq/build/devHash", "r+")
STABLEHASH = open("/var/www/tribiq/build/stableHash", "r+")

process1 = Popen("cd /home/git/bbcb.git && git log", stdout=PIPE, stderr=PIPE, shell=True)
(stdout1, stderr1) = process1.communicate()
if stderr1 == "":
   hash1 = stdout1.split("\n")[0].split(" ")[1]
   if STABLEHASH.readline().strip() == hash1:
      rebuildStable = False
      #print "No new commits in 'master'<br>"
   else:
      rebuildStable = True
      #print "New commit in 'master', need to run build pipeline<br>"
else:
   print stderr1

process2 = Popen("cd /home/git/bbcb.git && git log development", stdout=PIPE, stderr=PIPE, shell=True)
(stdout2, stderr2) = process2.communicate()
if stderr2 == "":
   hash2 = stdout2.split("\n")[0].split(" ")[1]
   if DEVHASH.readline().strip() == hash2:
      rebuildDev = False
      #print "No new commits in 'developent'<br>"
   else:
      rebuildDev = True
      #print "New commit in 'development', need to run build pipeline<br>"
else:
   print stderr2


def log(out, err):
   if out != "":   
      for line1 in out.split("\n"):
         print line1 + "<br>"
   if err != "":
      for line2 in err.split("\n"):
         print line2 + "<br>"

#### BUILDING STABLE


if rebuildStable:
   print "Building from 'master' at " + datetime.datetime.now().isoformat() + "<br>"
   print "The build pipeline for 'master' brunch is not developed..."
   # STABLEHASH.seek(0)
   # STABLEHASH.write(hash1)
   # STABLEHASH.truncate()

#### BUILDING DEVELOPMENT
if rebuildDev:

   print "<br><b>Building from 'development' at " + datetime.datetime.now().isoformat() + "</b><br>"
   print "Trying to check repository state...<br>"
   try:
       f = open('/home/git/bbcb.git.lock', 'r')
   except IOError:
       print "Repository is not locked. Continuing.<br>"
   else:
       print "Repository is locked (possibly because of sync process).<br>Aborting.<br>"
       f.close()
       sys.exit()

   print "Trying to clone repository into temporary folder...<br>"

   process1 = Popen("cd /var/www/tribiq/build && git clone /home/git/bbcb.git bb", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout1, stderr1) = process1.communicate()
   log(stdout1, stderr1)

   print "Checking out development...<br>"
   process2 = Popen("cd /var/www/tribiq/build/bb && git checkout development", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout2, stderr2) = process2.communicate()
   log(stdout2, stderr2)

   print "Building BlackBox...<br>"
   process3 = Popen("cd /var/www/tribiq/build/bb && /usr/local/bin/wine dev0.exe < build.txt 2>&1", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout3, stderr3) = process3.communicate()
   log(stdout3, stderr3)

   print "Building Setup File...<br>"
   process4 = Popen("cd /var/www/tribiq/build/bb && mv Code System/ && mv Sym System/ && cd appbuild && /usr/local/bin/iscc - < ./BlackBox.iss", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout4, stderr4) = process4.communicate()
   log(stdout4, stderr4)

   IN = open("/var/www/tribiq/build/bb/appbuild/BlackBox.iss")

   for line in IN:
      if line.split("=")[0] == "AppVersion":
         version =  line.split("=")[1].strip()
   print "The version of build: " + version + "<br>"

   print "Moving Setup File to dev folder...<br>"
   process5 = Popen("mv /var/www/tribiq/build/bb/appbuild/Output/setup.exe /var/www/tribiq/dev/blackbox-" + version + "-setup.exe", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout5, stderr5) = process5.communicate()
   log(stdout5, stderr5)

   if stderr5 == "":
      print "Hash for 'development' brunch updated<br>"
      DEVHASH.seek(0)
      DEVHASH.write(hash2)
      DEVHASH.truncate()

   print "Cleaning...<br>"
   process6 = Popen("cd /var/www/tribiq/build && rm bb -R", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout6, stderr6) = process6.communicate()
   log(stdout6, stderr6)

Автор:  Info21 [ Вторник, 22 Июль, 2014 06:40 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Вся эта ... материя, она действительно нужна?

Автор:  Иван Денисов [ Вторник, 22 Июль, 2014 08:12 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Info21 писал(а):
Вся эта ... материя, она действительно нужна?

Время покажет, это идея Джозефа, но я с ним в итоге согласился, глядя на сообщество. Многим лень разбираться с репозиториями, а чтобы «быть в теме», надо всегда иметь возможность поставить на комп последнюю «горячую» версию. К тому-же, тот факт, что среда каждый раз компилируется из исходников — это своеобразный тест и гарантия, что исходники соответствуют машинным кодам.

Наличие горячих сборок снижает порог для любопытствующих поглядеть как там дела с разработкой. Еще стоит добавить упаковку в архив, чтобы не было необходимости устанавливать.

Автор:  Info21 [ Среда, 23 Июль, 2014 00:10 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Репозиторий тут (в этой ... материи) как-то виноват?

Автор:  Иван Денисов [ Среда, 23 Июль, 2014 05:04 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Info21 писал(а):
Репозиторий тут (в этой ... материи) как-то виноват?

Я не совсем понимаю вопрос :)

История такая. Центр таки решился, что нужно иметь некое хранилище в сети, был выбран GitHub. А дальше есть два варианта:

1. Хранить там всю среду целиком
2. Хранить в хранилище исходный код

Я всегда пользовался первым методом для простоты, но второй метод более грамотный и имеет ряд преимуществ. Однако создаются сложности для разработчика, ведь эти исходные коды нужно собирать самой актуальной версией компилятора. Для этого я использовал наработки Александра Ширяева с консольным компилятором, добавил DevComDebug и убрал размещение консоли WinApi.AllocConsole. В таком виде этот компилятор может собирать среду из исходных кодов хранилища и на стороне сервера при создании файлов установки.

Если ведется доработка компилятора, то сам консольный компилятор рекурсивно должен быть пересобран новым вариантом компилятора из GUI среды и обновлен в хранилище. Все инструменты для этого уже сейчас в хранилище.

Вот такая история.

Автор:  Info21 [ Среда, 23 Июль, 2014 20:29 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Иван Денисов писал(а):
Info21 писал(а):
Репозиторий тут (в этой ... материи) как-то виноват?

Я не совсем понимаю вопрос :)
...
Вот такая история.
Спасибо:

Имеет место преодоление чужеродности репозитория и ББ (или Оберонов вообще?).

Это правильно?

Автор:  Иван Денисов [ Среда, 23 Июль, 2014 21:23 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Info21 писал(а):
Имеет место преодоление чужеродности репозитория и ББ (или Оберонов вообще?).

Если говорить про то, что хранилища предполагают плоский текст, а исходники Блэкбокса в бинарном формате, то частично эту проблему решили ранее. Был применен известный конвертер "odcread" как предварительный конвертер бинарных файлов для многих операций с плоским текстом системы контроля версий Git. Поэтому простая синхронизация любого Git хранилища со специально настроенным сервером решает эту проблему в вебе, а локально нужно лишь установить "odcread" и поправить два конфигурационных файла проекта.

Сейчас была решена скорее другая задача.
Дано: исходники компилятора, необходимость их распространять потенциальным разработчикам и автоматически компилировать на Debian сервере без GUI актуальной версией компилятора для создания программ установки среды.
Найти: техническое решение, которое позволило бы иметь идентичный Блэкбокс при ручном развертывании исходников и при автоматической компиляции на сервере.
Решение: хранение вместе с исходными кодами самого актуального консольного варианта компилятора, собранного из этих-же исходников. Автоматизированный алгоритм сборки Блэкбокса на сервере в виде программы для установки с использованием этого консольного компилятора.

Автор:  Info21 [ Четверг, 24 Июль, 2014 10:58 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Спасибо.

Автор:  Роман М. [ Четверг, 24 Июль, 2014 12:48 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Иван Денисов писал(а):
Вот, таки настроил автосборку установочников!

Вот тут лежат результаты:
http://blackboxframework.org/dev

Тут больше деталей:
http://redmine.blackboxframework.org/issues/3#note-6


Получился вот такой файл примерно, это некий начальный вариант, который предполагается улучшать. Ваши предложения приветствуются!

Код:
...

В качестве демонстрации возможностей подойдёт, однако вшитые пути и команды программ нужно отделить в первую очередь.

Автор:  Иван Денисов [ Четверг, 24 Июль, 2014 14:26 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Роман М. писал(а):
В качестве демонстрации возможностей подойдёт, однако вшитые пути и команды программ нужно отделить в первую очередь.


Да, согласен, что это сейчас на время, показать, что работает. Надо будет завести переменные под все пути и программы для начала. Скрипт будет удобнее отлаживать, когда хоть какое-то движение в коде начнутся :) Пока у меня перерыв в направлении Центра, надо другую работу доделать, которой накопилось в огромном количестве. Если Роман ты причешешь скрипт, будет замечательно.

Автор:  id_ler [ Вторник, 29 Июль, 2014 11:18 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Info21 писал(а):
Имеет место преодоление чужеродности репозитория и ББ (или Оберонов вообще?).

Это правильно?

Эволюция программиста :D Уровень Master Programmer следует за Seasoned professional, где платят построчно.

Автор:  Иван Денисов [ Пятница, 01 Август, 2014 00:29 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Вот новый скрипт, теперь он еще пакует в ZIP и номер сборки делает инкриментом, прописывая его в скрипт сборки установочника.

Код:
#!/usr/bin/python

from subprocess import Popen, PIPE
import sys, datetime, fileinput

buildDir = "/var/www/tribiq/build"
localRepository = "/var/www/git/bbcb.git"
devDir = "/var/www/tribiq/dev"
stableDir = "/var/www/tribiq/stable"
wine = "/usr/local/bin/wine"
iscc = "/usr/local/bin/iscc"


# this files contains hashes of last commits in master and development branches
DEVHASH = open(buildDir + "/devHash", "r+")
STABLEHASH = open(buildDir + "/stableHash", "r+")

process1 = Popen("cd " + localRepository + " && git log", stdout=PIPE, stderr=PIPE, shell=True)
(stdout1, stderr1) = process1.communicate()
if stderr1 == "":
   hash1 = stdout1.split("\n")[0].split(" ")[1]
   if STABLEHASH.readline().strip() == hash1:
      rebuildStable = False
      #print "No new commits in 'master'<br>"
   else:
      rebuildStable = True
      print "New commit in 'master', need to run build pipeline<br>"
else:
   print stderr1

process2 = Popen("cd " + localRepository + " && git log development", stdout=PIPE, stderr=PIPE, shell=True)
(stdout2, stderr2) = process2.communicate()
if stderr2 == "":
   hash2 = stdout2.split("\n")[0].split(" ")[1]
   if DEVHASH.readline().strip() == hash2:
      rebuildDev = False
      # print "No new commits in 'developent'<br>"
   else:
      rebuildDev = True
      print "New commit in 'development', need to run build pipeline<br>"
else:
   print stderr2


def log(out, err):
   if out != "":   
      for line1 in out.split("\n"):
         print line1 + "<br>"
   if err != "":
      for line2 in err.split("\n"):
         print line2 + "<br>"

#### BUILDING STABLE
if rebuildStable:
   print "Building from 'master' at " + datetime.datetime.now().isoformat() + "<br>"
   print "The build pipeline for 'master' branch is not developed..."
   # STABLEHASH.seek(0)
   # STABLEHASH.write(hash1)
   # STABLEHASH.truncate()


#### BUILDING DEVELOPMENT
if rebuildDev:

   print "<br><b>Building from 'development' at " + datetime.datetime.now().isoformat() + "</b><br>"
   print "Trying to check repository state...<br>"
   try:
       f = open(localRepository + ".lock", "r")
   except IOError:
       print "Repository is not locked. Continuing.<br>"
   else:
       print "Repository is locked (possibly because of sync process).<br>Aborting.<br>"
       f.close()
       sys.exit()

   print "Trying to clone repository into temporary folder...<br>"

   process1 = Popen("cd " + buildDir + " && git clone " + localRepository + " bb", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout1, stderr1) = process1.communicate()
   log(stdout1, stderr1)

   print "Checking out development...<br>"
   process2 = Popen("cd " + buildDir + "/bb && git checkout development", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout2, stderr2) = process2.communicate()
   log(stdout2, stderr2)

   print "Building BlackBox...<br>"
   process3 = Popen("cd " + buildDir + "/bb && " + wine + " dev0.exe < build.txt 2>&1", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout3, stderr3) = process3.communicate()
   log(stdout3, stderr3)

   NUMBER = open("number", "r+")
   buildNum = int(NUMBER.readline().strip())
   IN = open("bb/appbuild/BlackBox.iss", "r")
   lines = IN.readlines()
   IN.close()
   OUT = open("bb/appbuild/BlackBox.iss", "w")
   n = 0
   for line in lines:
      if n == 1:
         version = line.split('"')[1]
      n = n + 1
      if line[:18] == "VersionInfoVersion":
         print >> OUT, line.strip() + str(buildNum).zfill(3)
      else:
         print >> OUT, line,
   OUT.close()
   

   version = version + "." + str(buildNum).zfill(3)

   print "The version of build: " + version + "<br>"

   print "Building Setup File...<br>"
   process4 = Popen("cd " + buildDir + "/bb && mv Code System/ && mv Sym System/ && cd appbuild && " + iscc + " - < ./BlackBox.iss", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout4, stderr4) = process4.communicate()
   log(stdout4, stderr4)


   print "Moving Setup File to dev folder...<br>"
   process5 = Popen("mv " + buildDir + "/bb/appbuild/Output/setup.exe " + devDir + "/blackbox-" + version + "-setup.exe", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout5, stderr5) = process5.communicate()
   log(stdout5, stderr5)

   if stderr5 == "":
      print "Hash for 'development' branch updated<br>"
      DEVHASH.seek(0)
      DEVHASH.write(hash2)
      DEVHASH.truncate()
      print "Build number updated to " + str(buildNum + 1) + "<br>"
      NUMBER.seek(0)
      NUMBER.write(str(buildNum+1))
      NUMBER.truncate()



   print "Zipping...<br>"
   process6 = Popen("cd " + buildDir + "/bb && rm -R *.txt appbuild dev0.exe && zip -r ../../dev/blackbox-" + version + ".zip *", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout6, stderr6) = process6.communicate()
   log(stdout6, stderr6)



   print "Cleaning...<br>"
        process7 = Popen("cd " + buildDir + " && rm bb -R", stdout=PIPE, stderr=PIPE, shell=True)
        (stdout7, stderr7) = process7.communicate()
        log(stdout6, stderr7)

Автор:  Иван Денисов [ Пятница, 01 Август, 2014 06:08 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

На свежую голову утром нашел еще несколько косяков, поправил.

Скрипт каждые пять минут запускается с помощью Cron:
Код:
*/5 * * * * www-data /var/www/tribiq/build/build.py >> /var/www/tribiq/dev/buildlog.html 2>&1


Результат работы скрипта тут: http://blackboxframework.org/dev/ (см. версию 006)

Код:
#!/usr/bin/python

from subprocess import Popen, PIPE
import sys, datetime, fileinput

buildDir = "/var/www/tribiq/build"
localRepository = "/var/www/git/bbcb.git"
devDir = "/var/www/tribiq/dev"
stableDir = "/var/www/tribiq/stable"
wine = "/usr/local/bin/wine"
iscc = "/usr/local/bin/iscc"


# this files contains hashes of last commits in master and development branches
DEVHASH = open(buildDir + "/devHash", "r+")
STABLEHASH = open(buildDir + "/stableHash", "r+")

process1 = Popen("cd " + localRepository + " && git log", stdout=PIPE, stderr=PIPE, shell=True)
(stdout1, stderr1) = process1.communicate()
if stderr1 == "":
   hash1 = stdout1.split("\n")[0].split(" ")[1]
   if STABLEHASH.readline().strip() == hash1:
      rebuildStable = False
      #print "No new commits in 'master'<br>"
   else:
      rebuildStable = True
      print "New commit in 'master', need to run build pipeline<br>"
else:
   print stderr1

process2 = Popen("cd " + localRepository + " && git log development", stdout=PIPE, stderr=PIPE, shell=True)
(stdout2, stderr2) = process2.communicate()
if stderr2 == "":
   hash2 = stdout2.split("\n")[0].split(" ")[1]
   if DEVHASH.readline().strip() == hash2:
      rebuildDev = False
      # print "No new commits in 'developent'<br>"
   else:
      rebuildDev = True
      print "New commit in 'development', need to run build pipeline<br>"
else:
   print stderr2


def log(out, err):
   if out != "":   
      for line1 in out.split("\n"):
         print line1 + "<br>"
   if err != "":
      for line2 in err.split("\n"):
         print line2 + "<br>"

#### BUILDING STABLE
if rebuildStable:
   print "Building from 'master' at " + datetime.datetime.now().isoformat() + "<br>"
   print "The build pipeline for 'master' branch is not developed..."
   # STABLEHASH.seek(0)
   # STABLEHASH.write(hash1)
   # STABLEHASH.truncate()


#### BUILDING DEVELOPMENT
if rebuildDev:

   print "<br><br><b>Building from 'development' at " + datetime.datetime.now().isoformat() + "</b><br>"
   print "Trying to check repository state...<br>"
   try:
      f = open(localRepository + ".lock", "r")
   except IOError:
      print "Repository is not locked. Continuing.<br>"
   else:
      print "Repository is locked (possibly because of sync process).<br>Aborting.<br>"
      f.close()
      sys.exit()

   print "Trying to clone repository into temporary folder...<br>"

   process1 = Popen("cd " + buildDir + " && git clone " + localRepository + " bb", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout1, stderr1) = process1.communicate()
   log(stdout1, stderr1)

   print "Checking out development...<br>"
   process2 = Popen("cd " + buildDir + "/bb && git checkout development", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout2, stderr2) = process2.communicate()
   log(stdout2, stderr2)

   print "Building BlackBox...<br>"
   process3 = Popen("cd " + buildDir + "/bb && " + wine + " dev0.exe < build.txt 2>&1", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout3, stderr3) = process3.communicate()
   log(stdout3, stderr3)

   NUMBER = open(buildDir + "/number", "r+")
   buildNum = int(NUMBER.readline().strip())
   IN = open(buildDir + "/bb/appbuild/BlackBox.iss", "r")
   lines = IN.readlines()
   IN.close()
   OUT = open(buildDir + "/bb/appbuild/BlackBox.iss", "w")
   n = 0
   for line in lines:
      if n == 1:
         version = line.split('"')[1]
      n = n + 1
      if line[:18] == "VersionInfoVersion":
         print >> OUT, line.strip() + str(buildNum).zfill(3)
      else:
         print >> OUT, line,
   OUT.close()
   

   version = version + "." + str(buildNum).zfill(3)

   print "The version of build: " + version + "<br>"

   print "Building Setup File...<br>"
   process4 = Popen("cd " + buildDir + "/bb && mv Code System/ && mv Sym System/ && cd appbuild && " + iscc + " - < ./BlackBox.iss", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout4, stderr4) = process4.communicate()
   log(stdout4, stderr4)


   print "Moving Setup File to dev folder...<br>"
   process5 = Popen("mv " + buildDir + "/bb/appbuild/Output/setup.exe " + devDir + "/blackbox-" + version + "-setup.exe", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout5, stderr5) = process5.communicate()
   log(stdout5, stderr5)

   if stderr5 == "":
      print "Hash for 'development' branch updated<br>"
      DEVHASH.seek(0)
      DEVHASH.write(hash2)
      DEVHASH.truncate()
      print "Build number updated to " + str(buildNum + 1) + "<br>"
      NUMBER.seek(0)
      NUMBER.write(str(buildNum+1))
      NUMBER.truncate()


   print "Zipping...<br>"
   process6 = Popen("cd " + buildDir + "/bb && rm -R *.txt appbuild dev0.exe && zip -r ../../dev/blackbox-" + version + ".zip *", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout6, stderr6) = process6.communicate()
   log(stdout6, stderr6)


   print "Cleaning...<br><br><br>"
   process7 = Popen("cd " + buildDir + " && rm bb -R", stdout=PIPE, stderr=PIPE, shell=True)
   (stdout7, stderr7) = process7.communicate()
   log(stdout7, stderr7)

Автор:  Info21 [ Среда, 06 Август, 2014 23:30 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Не верю (с)

Автор:  Иван Денисов [ Четверг, 07 Август, 2014 00:26 ]
Заголовок сообщения:  Re: Автоматизация сборки БлэкБокс

Info21 писал(а):
Не верю (с)

В общем, щепки летят. Я пустил Джозефа на сервер, сейчас он там все переделает малость. У него новая мысль, что собирать надо из главной ветки, 'development' удалить, а изменения предлагать и тестировать в тематических ветках.

Страница 1 из 1 Часовой пояс: UTC + 3 часа
Powered by phpBB® Forum Software © phpBB Group
https://www.phpbb.com/